From b16efa4515dd7fe49e47217f41930e8350efb7fd Mon Sep 17 00:00:00 2001 From: Jeff West Date: Fri, 31 Jul 2026 05:23:50 -0500 Subject: [PATCH 1/2] Reconcile OpenAPI 3.27.0 spec drift (v9.0.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Syncs core OpenAPI 3.26.0 → 3.27.0 and matching perps/SCM content. Breaking: remove subaccounts.transfer_position after upstream deleted POST /portfolio/subaccounts/positions/transfer. Additive: live_data.get_event, DecreaseOrderV2Request.market_ticker, Series.exchange_index, WS MarketLifecyclePayload.exchange_index, perps fcm.create_subtrader, and Klear subtrader groups + settlement group fields. Closes #492 --- CHANGELOG.md | 50 ++ CLAUDE.md | 2 +- README.md | 2 +- ROADMAP.md | 5 + docs/index.md | 2 +- docs/migration.md | 54 ++ docs/perps.md | 14 +- docs/reference.md | 1 - docs/request-models.md | 1 - docs/resources/live-data.md | 43 +- docs/resources/subaccounts.md | 29 +- kalshi/__init__.py | 10 +- kalshi/_contract_map.py | 43 +- kalshi/models/__init__.py | 8 +- kalshi/models/live_data.py | 26 + kalshi/models/orders.py | 5 + kalshi/models/series.py | 2 + kalshi/models/subaccounts.py | 48 +- kalshi/perps/async_client.py | 2 + kalshi/perps/client.py | 2 + kalshi/perps/klear/models/__init__.py | 10 + kalshi/perps/klear/models/margin.py | 66 +- kalshi/perps/klear/resources/_base.py | 52 +- kalshi/perps/klear/resources/margin.py | 136 ++++- kalshi/perps/models/fcm.py | 26 + kalshi/perps/resources/fcm.py | 100 ++++ kalshi/resources/live_data.py | 42 ++ kalshi/resources/subaccounts.py | 168 +----- kalshi/ws/models/market_lifecycle.py | 3 + pyproject.toml | 2 +- specs/asyncapi.yaml | 12 + specs/openapi.yaml | 234 ++++---- specs/perps_openapi.yaml | 795 +++++++++---------------- specs/perps_scm_openapi.yaml | 168 +++++- tests/_contract_support.py | 41 +- tests/integration/test_subaccounts.py | 41 -- tests/perps/klear/test_margin.py | 81 +++ tests/perps/test_fcm.py | 70 +++ tests/test_contracts.py | 14 +- tests/test_live_data.py | 41 ++ tests/test_orders.py | 25 + tests/test_subaccounts.py | 200 ------- 42 files changed, 1502 insertions(+), 1174 deletions(-) create mode 100644 kalshi/perps/models/fcm.py create mode 100644 kalshi/perps/resources/fcm.py create mode 100644 tests/perps/test_fcm.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d33f25f..6124762f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,56 @@ All notable changes to kalshi-sdk will be documented in this file. +## 9.0.0 — 2026-07-31 + +Syncs upstream core OpenAPI **3.26.0 → 3.27.0** (paths stay 92; 103 operations / +102 mapped) and the matching perps + SCM OpenAPI content (Closes #492). +**Breaking** for callers of the subaccount position-transfer API. + +### Removed (breaking) + +- **`subaccounts.transfer_position()`** (sync + async) and models + `ApplySubaccountPositionTransferRequest` / + `ApplySubaccountPositionTransferResponse`. Upstream deleted + `POST /portfolio/subaccounts/positions/transfer` and the matching schemas + from OpenAPI 3.27.0. Cash `transfer()` is unchanged. + +### Added + +- **`live_data.get_event(event_ticker, *, range=None)`** (sync + async) — + `GET /live_data/events/{event_ticker}`. Returns `EventLiveData` (`type`, + `details`, optional `is_historical` / `default_range` / `range_options`). + Models: `EventLiveData`, `GetEventLiveDataResponse`. +- **`DecreaseOrderV2Request.market_ticker`** (`str | None`) — required by the + server when `exchange_index` is `-1` (auto-route by ticker). +- **`Series.exchange_index`** (`int | None`) — exchange shard for the series. +- **WS** `MarketLifecyclePayload.exchange_index` (`int | None`) — optional on + market-lifecycle `created` events (AsyncAPI). +- **Perps** `PerpsClient.fcm.create_subtrader(...)` (sync + async) — + `POST /margin/fcm/subtraders`. Body: + `CreateMarginFCMSubtraderRequest(subtrader_suffix=...)` (`^[a-z0-9]{1,16}$`); + returns `CreateMarginFCMSubtraderResponse.subtrader_id`. +- **Klear/SCM** subtrader groups on `KlearClient.margin` (sync + async): + - `list_subtrader_groups()` — `GET /fcm/margin/subtrader_groups` + - `create_subtrader_group(subtrader_ids=...)` — `POST /fcm/margin/subtrader_groups` + - `update_subtrader_group(group_id, subtrader_ids=...)` — `PUT .../{group_id}` + - `delete_subtrader_group(group_id)` — `DELETE .../{group_id}` + Models: `MarginSubtraderGroup`, `GetMarginSubtraderGroupsResponse`, + `CreateMarginSubtraderGroupRequest` / `Response`, + `UpdateMarginSubtraderGroupRequest`. +- **Klear settlement estimates**: optional `group_breakdowns` / + `omitted_group_count` on `GetSettlementEstimateResponse` and + `AssetClassSettlementEstimate`; optional `margin_group_id` on + `MaintenanceMarginDetail`. + +### Spec notes + +- Core OpenAPI `info.version` **3.27.0** (103 operations / 102 mapped). Still + unimplemented: `POST /portfolio/intra_exchange_instance_transfer` (use + `PerpsClient.transfers.transfer_instance()` on the margin product). +- Perps OpenAPI: +1 operation (`POST /margin/fcm/subtraders`). +- Perps SCM OpenAPI: +4 subtrader-group operations. + ## 8.0.0 — 2026-07-27 Reconciles upstream core OpenAPI / AsyncAPI content under version string diff --git a/CLAUDE.md b/CLAUDE.md index a6c536c1..5bfaff92 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -122,7 +122,7 @@ tests/ ## API Reference -- OpenAPI spec: https://docs.kalshi.com/openapi.yaml (v3.26.0, 103 operations; 102 mapped in the core SDK — `POST /portfolio/intra_exchange_instance_transfer` is currently not available upstream) +- OpenAPI spec: https://docs.kalshi.com/openapi.yaml (v3.27.0, 103 operations; 102 mapped in the core SDK — `POST /portfolio/intra_exchange_instance_transfer` is currently not available upstream) - 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 53b36ac7..135d77b2 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ A professional, spec-first Python SDK for the [Kalshi](https://kalshi.com) predi [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Type checked: mypy strict](https://img.shields.io/badge/mypy-strict-blue.svg)](https://mypy.readthedocs.io/) -- **Full coverage** of the Kalshi REST API (102 operations across 19 resources, OpenAPI v3.26.0) and WebSocket API (12 typed `subscribe_*` channels + 2 escape-hatch). +- **Full coverage** of the Kalshi REST API (102 operations across 19 resources, OpenAPI v3.27.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 (11 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/*` — the only order-write surface. diff --git a/ROADMAP.md b/ROADMAP.md index 06445f92..485c0112 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,6 +2,11 @@ ## Shipped +- **v9.0.0 (2026-07-31)** — OpenAPI sync 3.26.0 → 3.27.0 (#492). **Breaking:** + removed `subaccounts.transfer_position()` after upstream deleted position + transfers. Additive: `live_data.get_event()`, + `DecreaseOrderV2Request.market_ticker`, `Series.exchange_index`, perps + `fcm.create_subtrader()`, Klear subtrader groups + settlement group fields. - **v8.0.0 (2026-07-27)** — Spec-drift reconcile under OpenAPI 3.26.0 (#489 / #490). **Breaking:** removed the settlement-advance subaccount surface added in v7.4.0 (`lock_settlement_advance` / diff --git a/docs/index.md b/docs/index.md index 5fe11057..0d0347e8 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** — 102 operations across 19 resources (OpenAPI v3.26.0), +- **Full REST coverage** — 102 operations across 19 resources (OpenAPI v3.27.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 271fc982..731eff49 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1,5 +1,59 @@ # Migration +## v8.0 → v9.0.0 + +Syncs the SDK to core OpenAPI **3.27.0** (and the matching perps / SCM +content) after Kalshi removed subaccount **position** transfers and added +event-keyed live data plus FCM/SCM group surfaces (Closes #492). +**Breaking** for callers of `subaccounts.transfer_position()`. + +### Removed + +- **`subaccounts.transfer_position()`** (sync + async) and + `ApplySubaccountPositionTransferRequest` / + `ApplySubaccountPositionTransferResponse`. Upstream deleted + `POST /portfolio/subaccounts/positions/transfer` and the matching schemas. + +```python +# No longer available — the upstream endpoint 404s: +# client.subaccounts.transfer_position( +# client_transfer_id=..., +# from_subaccount=0, +# to_subaccount=1, +# market_ticker="...", +# side="yes", +# count=1, +# price=Decimal("0.50"), +# ) + +# Cash transfers are unchanged: +client.subaccounts.transfer( + client_transfer_id=..., + from_subaccount=0, + to_subaccount=1, + amount_cents=500, +) +``` + +### Added (non-breaking) + +- **`live_data.get_event(event_ticker, *, range=None)`** → `EventLiveData` + (`GET /live_data/events/{event_ticker}`). +- **`DecreaseOrderV2Request.market_ticker`** (optional; required when + `exchange_index=-1`). +- **`Series.exchange_index`** (optional). +- **WS** `MarketLifecyclePayload.exchange_index` (optional). +- **Perps** `fcm.create_subtrader(subtrader_suffix=...)` + (`POST /margin/fcm/subtraders`). +- **Klear/SCM** subtrader groups: `list_subtrader_groups`, + `create_subtrader_group`, `update_subtrader_group`, + `delete_subtrader_group` under `klear.margin`, plus optional + `group_breakdowns` / `omitted_group_count` on settlement estimates and + `margin_group_id` on `MaintenanceMarginDetail`. + +See the [changelog](https://github.com/TexasCoding/kalshi-python-sdk/blob/main/CHANGELOG.md) +for the full list. + ## v7.4 → v8.0.0 Reconciles upstream core OpenAPI / AsyncAPI content under version string diff --git a/docs/perps.md b/docs/perps.md index fff7e391..612d2afa 100644 --- a/docs/perps.md +++ b/docs/perps.md @@ -66,6 +66,7 @@ async with AsyncPerpsClient.from_env(demo=True) as perps: | `margin` | `balance()`, `risk()`, `notional_risk_limit()`, `fee_tiers()`, `api_limits()` | | `funding` | `rate_estimate()`, `historical_rates()`, `history()` | | `transfers` | `transfer_instance()`, `create_subaccount()`, `transfer_subaccount()` | +| `fcm` | `create_subtrader(subtrader_suffix=...)` — `POST /margin/fcm/subtraders` | The margin order side is `bid` / `ask` (not the prediction API's `yes` / `no`). Orders create/cancel/decrease/amend are POSTs/DELETEs and are **never retried**. @@ -201,7 +202,18 @@ Settlement-estimate responses also expose optional `omitted_subtrader_count` (`int | None`, SDK v7.3.0) on `GetSettlementEstimateResponse` and each `AssetClassSettlementEstimate` — the number of subtraders left out of `subtrader_breakdowns` (their amounts remain -in `user_breakdown`). +in `user_breakdown`). SDK v9.0.0 adds optional `group_breakdowns` / +`omitted_group_count` for netted subtrader groups, and optional +`margin_group_id` on `MaintenanceMarginDetail`. + +Subtrader groups (SDK v9.0.0) — margined as one netted portfolio: + +```python +groups = klear.margin.list_subtrader_groups() +created = klear.margin.create_subtrader_group(subtrader_ids=["st-a", "st-b"]) +klear.margin.update_subtrader_group(created.group_id, subtrader_ids=["st-a", "st-c"]) +klear.margin.delete_subtrader_group(created.group_id) +``` Money fields on the Klear margin schemas are integer **centicents** (`1 USD = 10,000 centicents`); only the withdrawal `amount` is a fixed-point dollar string. diff --git a/docs/reference.md b/docs/reference.md index a5234faf..32be5c85 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -72,7 +72,6 @@ every exception class. ::: kalshi.models.subaccounts.ApplySubaccountTransferRequest -::: kalshi.models.subaccounts.ApplySubaccountPositionTransferRequest ::: kalshi.models.subaccounts.UpdateSubaccountNettingRequest diff --git a/docs/request-models.md b/docs/request-models.md index a79123eb..9451f3f5 100644 --- a/docs/request-models.md +++ b/docs/request-models.md @@ -68,7 +68,6 @@ exposed by each resource method stays in lockstep with the OpenAPI spec. | `client.order_groups.create` | `CreateOrderGroupRequest` | | `client.order_groups.update_limit` | `UpdateOrderGroupLimitRequest` | | `client.subaccounts.transfer` | `ApplySubaccountTransferRequest` | -| `client.subaccounts.transfer_position` | `ApplySubaccountPositionTransferRequest` | | `client.subaccounts.update_netting` | `UpdateSubaccountNettingRequest` | All are importable from the top-level `kalshi` package. diff --git a/docs/resources/live-data.md b/docs/resources/live-data.md index 1bea8d3f..2e14038d 100644 --- a/docs/resources/live-data.md +++ b/docs/resources/live-data.md @@ -1,8 +1,8 @@ # Live data -Real-time state attached to a [milestone](milestones.md) — score, clock, -period, weather, etc. Pair with milestones to render live UI alongside -Kalshi markets. +Real-time state for markets — either keyed by a [milestone](milestones.md) +(score, clock, period, weather, …) or by an **event ticker** (crypto charts, +commodity timeseries, weather observations). Public — no auth required. @@ -11,17 +11,32 @@ Public — no auth required. | Method | Endpoint | |---|---| | `get(milestone_id, *, include_player_stats=None)` | `GET /live_data/milestone/{milestone_id}` | +| `get_event(event_ticker, *, range=None)` | `GET /live_data/events/{event_ticker}` | | `batch(milestone_ids, *, include_player_stats=None)` | `GET /live_data/batch` | | `game_stats(milestone_id)` | `GET /live_data/milestone/{milestone_id}/game_stats` | -| `get_typed(live_data_type, milestone_id)` | `GET /live_data/{type}/milestone/{milestone_id}` (legacy) | +| `get_typed(milestone_type, milestone_id)` | `GET /live_data/{type}/milestone/{milestone_id}` (legacy) | ## Get one milestone's live data ```python live = client.live_data.get("ms_abc", include_player_stats=True) -print(live.live_data_type, live.payload) +print(live.type, live.milestone_id, live.details) ``` +`LiveData.details` is a loose `dict[str, Any]` — the shape varies by +`type` (football vs political race vs weather). + +## Get event-keyed live data + +```python +live = client.live_data.get_event("KXBTCD-25", range="1h") +print(live.type, live.details, live.default_range, live.range_options) +print(live.is_historical) # True for matured crypto snapshots +``` + +`EventLiveData` has no `milestone_id`. Optional `range` is a chart-window +hint (`15min`, `1h`, `1d`, …) when the underlying type supports it. + ## Batch (up to 100 milestones) ```python @@ -30,7 +45,7 @@ entries = client.live_data.batch( include_player_stats=False, ) for entry in entries: - print(entry.milestone_id, entry.payload) + print(entry.milestone_id, entry.type, entry.details) ``` `milestone_ids` is required and non-empty — passing `[]` raises `ValueError`. @@ -39,17 +54,18 @@ Cap: 100 ids per call. ## Game stats / play-by-play ```python -pbp = client.live_data.game_stats("ms_abc") -if pbp.pbp is None: +resp = client.live_data.game_stats("ms_abc") +if resp.pbp is None: print("no play-by-play for this milestone type") else: - for period in pbp.pbp.periods: - for play in period.plays: - print(play.timestamp, play.description) + for period in resp.pbp.periods: + for event in period.events: + print(event) # free-form dict; shape varies by sport ``` `game_stats` works only for sports milestones with play-by-play coverage. -Other milestone types return `pbp=None`. +Other milestone types return `pbp=None`. Each period's `events` is a list of +loose dicts (no fixed play schema upstream). ## Legacy `get_typed` @@ -59,7 +75,8 @@ live = client.live_data.get_typed("sports_game", "ms_abc") Prefer `get()` over `get_typed()`. The latter wraps the legacy `/live_data/{type}/milestone/{id}` path and is retained only for callers that -still depend on it. +still depend on it. The Python kwarg is `milestone_type` (not `type`) to avoid +shadowing the built-in; the wire path still uses `{type}`. ## Reference diff --git a/docs/resources/subaccounts.md b/docs/resources/subaccounts.md index c750e976..3be860e7 100644 --- a/docs/resources/subaccounts.md +++ b/docs/resources/subaccounts.md @@ -9,7 +9,6 @@ primary; `1`–`63` are numbered extras. Auth required throughout. |---|---| | `create(*, exchange_index=None)` | `POST /portfolio/subaccounts` | | `transfer(*, client_transfer_id, from_subaccount, to_subaccount, amount_cents)` | `POST /portfolio/subaccounts/transfer` | -| `transfer_position(*, client_transfer_id, from_subaccount, to_subaccount, market_ticker, side, count, price)` | `POST /portfolio/subaccounts/positions/transfer` | | `list_balances()` | `GET /portfolio/subaccounts/balances` | | `list_transfers(*, cursor=None, limit=None)` | `GET /portfolio/subaccounts/transfers` | | `list_all_transfers(*, limit=None, max_pages=None)` | walks `list_transfers` | @@ -50,27 +49,10 @@ client.subaccounts.transfer( `client_transfer_id` accepts a `UUID` or a `str`. On a network failure, retry with the same id; the server dedupes. -## Transfer a position between subaccounts - -Spec v3.23.0 added `transfer_position()` for moving open contracts (not cash) -between subaccounts. Unlike `transfer()`, it returns a `position_transfer_id`. -`price` (spec v3.24.0 renamed it from `price_cents`) is the per-contract cost -basis in **fixed-point dollars** (0–1.0) — pass a `Decimal`: - -```python -from decimal import Decimal - -resp = client.subaccounts.transfer_position( - client_transfer_id=uuid.uuid4(), # or str - from_subaccount=0, - to_subaccount=1, - market_ticker="KXBTC-25DEC31-B100000", - side="yes", # "yes" | "no" - count=10, # contracts (> 0) - price=Decimal("0.55"), # per-contract dollars, 0–1.0 -) -print(resp.position_transfer_id) -``` +!!! note "Position transfers removed in OpenAPI 3.27.0" + `subaccounts.transfer_position()` and the + `POST /portfolio/subaccounts/positions/transfer` endpoint were deleted + upstream and removed from the SDK in v9.0.0. Only cash transfers remain. ## List balances @@ -96,8 +78,7 @@ for t in client.subaccounts.list_all_transfers(): ``` Standard `Page[SubaccountTransfer]` pagination. `t.created_ts` is Unix -seconds. Rows are **cash transfers only** — position moves use -`transfer_position()` and are not listed here. +seconds. Rows are **cash transfers only**. ## Netting diff --git a/kalshi/__init__.py b/kalshi/__init__.py index c8045161..177e5992 100644 --- a/kalshi/__init__.py +++ b/kalshi/__init__.py @@ -38,8 +38,6 @@ Announcement, ApiKey, ApiUsageLevelGrant, - ApplySubaccountPositionTransferRequest, - ApplySubaccountPositionTransferResponse, ApplySubaccountTransferRequest, AssociatedEvent, Balance, @@ -77,6 +75,7 @@ Event, EventCandlesticks, EventFeeChange, + EventLiveData, EventMetadata, EventPosition, EventStatusLiteral, @@ -89,6 +88,7 @@ GetApiKeysResponse, GetBlockTradeProposalsResponse, GetCommunicationsIDResponse, + GetEventLiveDataResponse, GetFiltersBySportsResponse, GetGameStatsResponse, GetIncentiveProgramsResponse, @@ -203,8 +203,6 @@ "Announcement", "ApiKey", "ApiUsageLevelGrant", - "ApplySubaccountPositionTransferRequest", - "ApplySubaccountPositionTransferResponse", "ApplySubaccountTransferRequest", "AssociatedEvent", "AsyncBlockTradeProposalsResource", @@ -250,6 +248,7 @@ "Event", "EventCandlesticks", "EventFeeChange", + "EventLiveData", "EventMetadata", "EventPosition", "EventStatusLiteral", @@ -266,6 +265,7 @@ "GetApiKeysResponse", "GetBlockTradeProposalsResponse", "GetCommunicationsIDResponse", + "GetEventLiveDataResponse", "GetFiltersBySportsResponse", "GetGameStatsResponse", "GetIncentiveProgramsResponse", @@ -379,4 +379,4 @@ "Withdrawal", ] -__version__ = "8.0.0" +__version__ = "9.0.0" diff --git a/kalshi/_contract_map.py b/kalshi/_contract_map.py index bfdbbe4a..eaffbe77 100644 --- a/kalshi/_contract_map.py +++ b/kalshi/_contract_map.py @@ -174,14 +174,6 @@ class ContractEntry: sdk_model="kalshi.models.subaccounts.ApplySubaccountTransferRequest", spec_schema="ApplySubaccountTransferRequest", ), - ContractEntry( - sdk_model="kalshi.models.subaccounts.ApplySubaccountPositionTransferRequest", - spec_schema="ApplySubaccountPositionTransferRequest", - ), - ContractEntry( - sdk_model="kalshi.models.subaccounts.ApplySubaccountPositionTransferResponse", - spec_schema="ApplySubaccountPositionTransferResponse", - ), ContractEntry( sdk_model="kalshi.models.subaccounts.CreateSubaccountRequest", spec_schema="CreateSubaccountRequest", @@ -220,6 +212,11 @@ class ContractEntry: spec_schema="LiveData", notes="details is dict[str, Any] per spec additionalProperties:true", ), + ContractEntry( + sdk_model="kalshi.models.live_data.EventLiveData", + spec_schema="EventLiveData", + notes="event-keyed live data; details is dict[str, Any]", + ), ContractEntry( sdk_model="kalshi.models.markets.MarketCandlesticks", spec_schema="MarketCandlesticksResponse", @@ -799,6 +796,15 @@ class ContractEntry: sdk_model="kalshi.perps.models.transfers.CreateSubaccountResponse", spec_schema="CreateSubaccountResponse", ), + # ── perps FCM subtraders ── + ContractEntry( + sdk_model="kalshi.perps.models.fcm.CreateMarginFCMSubtraderRequest", + spec_schema="CreateMarginFCMSubtraderRequest", + ), + ContractEntry( + sdk_model="kalshi.perps.models.fcm.CreateMarginFCMSubtraderResponse", + spec_schema="CreateMarginFCMSubtraderResponse", + ), ] PERPS_SCM_CONTRACT_MAP: list[ContractEntry] = [ @@ -876,4 +882,25 @@ class ContractEntry: sdk_model="kalshi.perps.klear.models.margin.SettlementBalanceHistoryEntry", spec_schema="SettlementBalanceHistoryEntry", ), + # ── perps SCM subtrader groups ── + ContractEntry( + sdk_model="kalshi.perps.klear.models.margin.MarginSubtraderGroup", + spec_schema="MarginSubtraderGroup", + ), + ContractEntry( + sdk_model="kalshi.perps.klear.models.margin.GetMarginSubtraderGroupsResponse", + spec_schema="GetMarginSubtraderGroupsResponse", + ), + ContractEntry( + sdk_model="kalshi.perps.klear.models.margin.CreateMarginSubtraderGroupRequest", + spec_schema="CreateMarginSubtraderGroupRequest", + ), + ContractEntry( + sdk_model="kalshi.perps.klear.models.margin.CreateMarginSubtraderGroupResponse", + spec_schema="CreateMarginSubtraderGroupResponse", + ), + ContractEntry( + sdk_model="kalshi.perps.klear.models.margin.UpdateMarginSubtraderGroupRequest", + spec_schema="UpdateMarginSubtraderGroupRequest", + ), ] diff --git a/kalshi/models/__init__.py b/kalshi/models/__init__.py index 3c1907a0..dc19917e 100644 --- a/kalshi/models/__init__.py +++ b/kalshi/models/__init__.py @@ -68,6 +68,8 @@ IncentiveProgramTypeLiteral, ) from kalshi.models.live_data import ( + EventLiveData, + GetEventLiveDataResponse, GetGameStatsResponse, GetLiveDataResponse, GetLiveDatasResponse, @@ -165,8 +167,6 @@ StructuredTarget, ) from kalshi.models.subaccounts import ( - ApplySubaccountPositionTransferRequest, - ApplySubaccountPositionTransferResponse, ApplySubaccountTransferRequest, CreateSubaccountRequest, CreateSubaccountResponse, @@ -192,8 +192,6 @@ "Announcement", "ApiKey", "ApiUsageLevelGrant", - "ApplySubaccountPositionTransferRequest", - "ApplySubaccountPositionTransferResponse", "ApplySubaccountTransferRequest", "AssociatedEvent", "Balance", @@ -231,6 +229,7 @@ "Event", "EventCandlesticks", "EventFeeChange", + "EventLiveData", "EventMetadata", "EventPosition", "EventStatusLiteral", @@ -243,6 +242,7 @@ "GetApiKeysResponse", "GetBlockTradeProposalsResponse", "GetCommunicationsIDResponse", + "GetEventLiveDataResponse", "GetFiltersBySportsResponse", "GetGameStatsResponse", "GetIncentiveProgramsResponse", diff --git a/kalshi/models/live_data.py b/kalshi/models/live_data.py index fdbdb2bd..7e8e2961 100644 --- a/kalshi/models/live_data.py +++ b/kalshi/models/live_data.py @@ -26,6 +26,24 @@ class LiveData(BaseModel): model_config = {"extra": "allow"} +class EventLiveData(BaseModel): + """Live-data payload keyed by event ticker (not milestone). + + Spec ``EventLiveData`` (OpenAPI 3.27.0) — used for event-keyed series such as + crypto price charts, commodity timeseries, and weather observations. + ``type`` names the schema of ``details``. Unlike :class:`LiveData`, there is + no ``milestone_id``. + """ + + type: str + details: dict[str, Any] + is_historical: bool | None = None + default_range: str | None = None + range_options: list[str] | None = None + + model_config = {"extra": "allow"} + + class GetLiveDataResponse(BaseModel): """Response from GET /live_data/milestone/{milestone_id}.""" @@ -34,6 +52,14 @@ class GetLiveDataResponse(BaseModel): model_config = {"extra": "allow"} +class GetEventLiveDataResponse(BaseModel): + """Response from GET /live_data/events/{event_ticker}.""" + + live_data: EventLiveData + + model_config = {"extra": "allow"} + + class GetLiveDatasResponse(BaseModel): """Response from GET /live_data/batch — multiple milestones at once.""" diff --git a/kalshi/models/orders.py b/kalshi/models/orders.py index d0bd7131..5de97fa4 100644 --- a/kalshi/models/orders.py +++ b/kalshi/models/orders.py @@ -219,11 +219,16 @@ class DecreaseOrderV2Request(BaseModel): Spec marks all fields optional but server requires exactly one of ``reduce_by`` or ``reduce_to``. Enforced at construction. + + ``market_ticker`` (OpenAPI 3.27.0) is required when ``exchange_index`` is + ``-1`` (auto-route by ticker), matching cancel/batch-cancel. """ reduce_by: FixedPointCount | None = None reduce_to: FixedPointCount | None = None + # No ``ge=0``: ExchangeIndex permits ``-1`` to auto-route by market ticker. exchange_index: StrictInt | None = None + market_ticker: str | None = None model_config = {"extra": "forbid"} diff --git a/kalshi/models/series.py b/kalshi/models/series.py index d326d955..19a86252 100644 --- a/kalshi/models/series.py +++ b/kalshi/models/series.py @@ -35,6 +35,8 @@ class Series(BaseModel): validation_alias=AliasChoices("volume_fp", "volume"), ) last_updated_ts: AwareDatetime | None = None + # Spec 3.27.0: exchange shard for the series (optional; defaults server-side). + exchange_index: int | None = None model_config = {"extra": "allow", "populate_by_name": True} diff --git a/kalshi/models/subaccounts.py b/kalshi/models/subaccounts.py index f42eeffe..99b1c92d 100644 --- a/kalshi/models/subaccounts.py +++ b/kalshi/models/subaccounts.py @@ -7,7 +7,7 @@ from pydantic import BaseModel, Field -from kalshi.types import DollarDecimal, OrderPrice, StrictInt +from kalshi.types import DollarDecimal, StrictInt class CreateSubaccountRequest(BaseModel): @@ -57,46 +57,6 @@ class ApplySubaccountTransferRequest(BaseModel): model_config = {"extra": "forbid"} -class ApplySubaccountPositionTransferRequest(BaseModel): - """Body for POST /portfolio/subaccounts/positions/transfer (spec v3.24.0). - - Moves an open **position** (contracts) between subaccounts — distinct from - the cash-only :class:`ApplySubaccountTransferRequest`. ``price`` is the - per-contract price in **fixed-point dollars** (``0``-``1.0``) used to set the - cost basis on the destination subaccount; ``count`` is the number of contracts - and must be positive. ``from_subaccount`` / ``to_subaccount`` use ``0`` for the - primary account; the SDK enforces only the lower bound (``ge=0``), leaving the - upper bound to the server (mirrors :class:`ApplySubaccountTransferRequest`). - - Spec v3.24.0 renamed ``price_cents`` (integer cents) → ``price`` - (``FixedPointDollars``); pass a ``Decimal`` in dollars, e.g. ``Decimal("0.50")`` - for a 50¢ cost basis. Uses :data:`~kalshi.types.OrderPrice` so a negative or - sub-$0.0001-tick value fails at construction rather than as a server 400. - """ - - client_transfer_id: UUID - from_subaccount: StrictInt = Field(ge=0) - to_subaccount: StrictInt = Field(ge=0) - market_ticker: str - side: Literal["yes", "no"] - count: StrictInt = Field(gt=0) - price: OrderPrice - - model_config = {"extra": "forbid"} - - -class ApplySubaccountPositionTransferResponse(BaseModel): - """Response from POST /portfolio/subaccounts/positions/transfer (spec v3.23.0). - - ``position_transfer_id`` is the server-generated identifier for the applied - position transfer. - """ - - position_transfer_id: str - - model_config = {"extra": "allow"} - - class SubaccountBalance(BaseModel): """Balance for a single subaccount. @@ -135,10 +95,8 @@ class SubaccountTransfer(BaseModel): ``GET /portfolio/subaccounts/transfers`` to cash rows only. Upstream dropped ``transfer_type`` and the position-only fields (``market_ticker`` / ``side`` / ``count`` / ``price``) from this schema. - Position moves use :class:`ApplySubaccountPositionTransferRequest` / - :class:`ApplySubaccountPositionTransferResponse` on - ``POST /portfolio/subaccounts/positions/transfer`` — they are not listed - here. Fields removed from the wire schema are retained as optional + Spec 3.27.0 later removed ``POST /portfolio/subaccounts/positions/transfer`` + entirely. Fields removed from the wire schema are retained as optional (defensive optional-ization) so payloads from lagging servers still parse; new responses omit them. """ diff --git a/kalshi/perps/async_client.py b/kalshi/perps/async_client.py index 3d66dca6..a86ddac9 100644 --- a/kalshi/perps/async_client.py +++ b/kalshi/perps/async_client.py @@ -19,6 +19,7 @@ PerpsConfig, ) from kalshi.perps.resources.exchange import AsyncPerpsExchangeResource +from kalshi.perps.resources.fcm import AsyncFcmResource from kalshi.perps.resources.funding import AsyncFundingResource from kalshi.perps.resources.margin_account import AsyncMarginAccountResource from kalshi.perps.resources.markets import AsyncPerpsMarketsResource @@ -150,6 +151,7 @@ def __init__( self.margin = AsyncMarginAccountResource(self._transport) self.funding = AsyncFundingResource(self._transport) self.transfers = AsyncTransfersResource(self._transport) + self.fcm = AsyncFcmResource(self._transport) @property def is_authenticated(self) -> bool: diff --git a/kalshi/perps/client.py b/kalshi/perps/client.py index b434d770..be2190c0 100644 --- a/kalshi/perps/client.py +++ b/kalshi/perps/client.py @@ -19,6 +19,7 @@ PerpsConfig, ) from kalshi.perps.resources.exchange import PerpsExchangeResource +from kalshi.perps.resources.fcm import FcmResource from kalshi.perps.resources.funding import FundingResource from kalshi.perps.resources.margin_account import MarginAccountResource from kalshi.perps.resources.markets import PerpsMarketsResource @@ -159,6 +160,7 @@ def __init__( self.margin = MarginAccountResource(self._transport) self.funding = FundingResource(self._transport) self.transfers = TransfersResource(self._transport) + self.fcm = FcmResource(self._transport) @property def is_authenticated(self) -> bool: diff --git a/kalshi/perps/klear/models/__init__.py b/kalshi/perps/klear/models/__init__.py index 0d02761f..7a4d04e3 100644 --- a/kalshi/perps/klear/models/__init__.py +++ b/kalshi/perps/klear/models/__init__.py @@ -6,10 +6,13 @@ from kalshi.perps.klear.models.margin import ( AssetClassLiteral, AssetClassSettlementEstimate, + CreateMarginSubtraderGroupRequest, + CreateMarginSubtraderGroupResponse, GetActiveMarginObligationResponse, GetActiveMarginObligationsResponse, GetGuarantyFundBalanceResponse, GetMarginReportsResponse, + GetMarginSubtraderGroupsResponse, GetObligationHistoryResponse, GetSettlementBalanceHistoryResponse, GetSettlementBalanceResponse, @@ -19,12 +22,14 @@ MaintenanceMarginDetail, MarginReport, MarginReportTypeLiteral, + MarginSubtraderGroup, MarketSettlementEstimate, ObligationEntry, ObligationReceiveInfo, SettlementBalanceHistoryEntry, SettlementDetail, SettlementEstimate, + UpdateMarginSubtraderGroupRequest, WithdrawalStatusLiteral, WithdrawSettlementBalanceRequest, WithdrawSettlementBalanceResponse, @@ -33,11 +38,14 @@ __all__ = [ "AssetClassLiteral", "AssetClassSettlementEstimate", + "CreateMarginSubtraderGroupRequest", + "CreateMarginSubtraderGroupResponse", "Error", "GetActiveMarginObligationResponse", "GetActiveMarginObligationsResponse", "GetGuarantyFundBalanceResponse", "GetMarginReportsResponse", + "GetMarginSubtraderGroupsResponse", "GetObligationHistoryResponse", "GetSettlementBalanceHistoryResponse", "GetSettlementBalanceResponse", @@ -47,12 +55,14 @@ "MaintenanceMarginDetail", "MarginReport", "MarginReportTypeLiteral", + "MarginSubtraderGroup", "MarketSettlementEstimate", "ObligationEntry", "ObligationReceiveInfo", "SettlementBalanceHistoryEntry", "SettlementDetail", "SettlementEstimate", + "UpdateMarginSubtraderGroupRequest", "WithdrawSettlementBalanceRequest", "WithdrawSettlementBalanceResponse", "WithdrawalStatusLiteral", diff --git a/kalshi/perps/klear/models/margin.py b/kalshi/perps/klear/models/margin.py index 741eac88..91a3212f 100644 --- a/kalshi/perps/klear/models/margin.py +++ b/kalshi/perps/klear/models/margin.py @@ -34,7 +34,7 @@ from decimal import Decimal from typing import Annotated, Literal -from pydantic import AfterValidator, AwareDatetime, BaseModel +from pydantic import AfterValidator, AwareDatetime, BaseModel, Field from kalshi.types import DollarDecimal, NullableList @@ -144,12 +144,14 @@ class MaintenanceMarginDetail(BaseModel): """Spec ``MaintenanceMarginDetail`` — maintenance-margin requirement + delta. ``subtrader_id`` may be an empty string when not populated. + ``margin_group_id`` is set when the subtrader is part of a subtrader group. """ id: str subtrader_id: str maintenance_margin_centicents: int maintenance_margin_delta_centicents: int + margin_group_id: str | None = None model_config = {"extra": "allow"} @@ -248,6 +250,10 @@ class GetSettlementEstimateResponse(BaseModel): ``omitted_subtrader_count`` is the number of subtraders omitted from ``subtrader_breakdowns`` (their amounts remain in ``user_breakdown``). + + ``group_breakdowns`` maps margin group ID → netted portfolio estimate; + ``omitted_group_count`` is how many groups were omitted from that map + (amounts still roll into ``user_breakdown``). """ user_breakdown: SettlementEstimate @@ -255,6 +261,8 @@ class GetSettlementEstimateResponse(BaseModel): prev_settlement_prices: dict[str, int] | None = None settlement_balance_centicents: int omitted_subtrader_count: int | None = None + group_breakdowns: dict[str, SettlementEstimate] | None = None + omitted_group_count: int | None = None model_config = {"extra": "allow"} @@ -285,6 +293,9 @@ class AssetClassSettlementEstimate(BaseModel): ``omitted_subtrader_count`` is the number of subtraders omitted from ``subtrader_breakdowns`` (their amounts remain in ``user_breakdown``). + + ``group_breakdowns`` / ``omitted_group_count`` mirror + :class:`GetSettlementEstimateResponse` for subtrader groups. """ next_runtime: AwareDatetime @@ -292,6 +303,8 @@ class AssetClassSettlementEstimate(BaseModel): subtrader_breakdowns: dict[str, SettlementEstimate] | None = None prev_settlement_prices: dict[str, int] | None = None omitted_subtrader_count: int | None = None + group_breakdowns: dict[str, SettlementEstimate] | None = None + omitted_group_count: int | None = None model_config = {"extra": "allow"} @@ -402,3 +415,54 @@ def has_next(self) -> bool: return bool(self.cursor) model_config = {"extra": "allow"} + + +# ── Spec sync: margin subtrader groups (perps SCM) ────────────────────────── + + +class MarginSubtraderGroup(BaseModel): + """Spec ``MarginSubtraderGroup`` — a netted portfolio of subtraders.""" + + group_id: str + member_subtrader_ids: NullableList[str] + + model_config = {"extra": "allow"} + + +class GetMarginSubtraderGroupsResponse(BaseModel): + """Response from GET /fcm/margin/subtrader_groups.""" + + groups: NullableList[MarginSubtraderGroup] + + model_config = {"extra": "allow"} + + +class CreateMarginSubtraderGroupRequest(BaseModel): + """Body for POST /fcm/margin/subtrader_groups. + + ``subtrader_ids`` must be non-empty; members must not already belong to + another group. Grouped subtraders are margined as one netted portfolio. + """ + + subtrader_ids: list[str] = Field(min_length=1) + + model_config = {"extra": "forbid"} + + +class CreateMarginSubtraderGroupResponse(BaseModel): + """Response from POST /fcm/margin/subtrader_groups.""" + + group_id: str + + model_config = {"extra": "allow"} + + +class UpdateMarginSubtraderGroupRequest(BaseModel): + """Body for PUT /fcm/margin/subtrader_groups/{group_id}. + + Full replacement membership list (not a patch). + """ + + subtrader_ids: list[str] = Field(min_length=1) + + model_config = {"extra": "forbid"} diff --git a/kalshi/perps/klear/resources/_base.py b/kalshi/perps/klear/resources/_base.py index 8e4faba4..e189402c 100644 --- a/kalshi/perps/klear/resources/_base.py +++ b/kalshi/perps/klear/resources/_base.py @@ -6,10 +6,8 @@ to merge the Klear Bearer header onto every request — the paginators (``_list``/``_list_all``) route through ``_get``, so they are covered too. -Only ``_get``/``_post`` are overridden because the entire Klear surface is -GET/POST. If a future spec revision adds a PUT/DELETE Klear endpoint, override -the matching base helper here too (otherwise its requests would go out -unauthenticated). +``_get``/``_post``/``_put``/``_delete`` are overridden so every Klear request +carries the Bearer header (subtrader-group CRUD uses PUT/DELETE). """ from __future__ import annotations @@ -65,6 +63,29 @@ def _post( path, params=params, json=json, extra_headers=self._with_auth(extra_headers) ) + def _put( + self, + path: str, + *, + params: dict[str, Any] | None = None, + json: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + ) -> dict[str, Any] | None: + return super()._put( + path, params=params, json=json, extra_headers=self._with_auth(extra_headers) + ) + + def _delete( + self, + path: str, + *, + params: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + ) -> dict[str, Any] | None: + return super()._delete( + path, params=params, extra_headers=self._with_auth(extra_headers) + ) + class KlearAsyncResource(AsyncResource): """Async Klear resource base — transport + Bearer header injection.""" @@ -98,3 +119,26 @@ async def _post( return await super()._post( path, params=params, json=json, extra_headers=self._with_auth(extra_headers) ) + + async def _put( + self, + path: str, + *, + params: dict[str, Any] | None = None, + json: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + ) -> dict[str, Any] | None: + return await super()._put( + path, params=params, json=json, extra_headers=self._with_auth(extra_headers) + ) + + async def _delete( + self, + path: str, + *, + params: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + ) -> dict[str, Any] | None: + return await super()._delete( + path, params=params, extra_headers=self._with_auth(extra_headers) + ) diff --git a/kalshi/perps/klear/resources/margin.py b/kalshi/perps/klear/resources/margin.py index b7ebd281..bbfbad49 100644 --- a/kalshi/perps/klear/resources/margin.py +++ b/kalshi/perps/klear/resources/margin.py @@ -32,21 +32,31 @@ from kalshi.models.common import Page from kalshi.perps.klear.models.margin import ( + CreateMarginSubtraderGroupRequest, + CreateMarginSubtraderGroupResponse, GetActiveMarginObligationResponse, GetActiveMarginObligationsResponse, GetGuarantyFundBalanceResponse, GetMarginReportsResponse, + GetMarginSubtraderGroupsResponse, GetSettlementBalanceResponse, GetSettlementBalanceWithdrawalResponse, GetSettlementEstimateByAssetClassResponse, GetSettlementEstimateResponse, ObligationEntry, SettlementBalanceHistoryEntry, + UpdateMarginSubtraderGroupRequest, WithdrawSettlementBalanceRequest, WithdrawSettlementBalanceResponse, ) from kalshi.perps.klear.resources._base import KlearAsyncResource, KlearSyncResource -from kalshi.resources._base import _params, _validate_limit, _validate_max_pages +from kalshi.resources._base import ( + _check_request_exclusive, + _params, + _seg, + _validate_limit, + _validate_max_pages, +) from kalshi.types import DollarDecimal, to_decimal @@ -258,6 +268,68 @@ def settlement_balance_withdrawal( ) return GetSettlementBalanceWithdrawalResponse.model_validate(data) + def list_subtrader_groups( + self, *, extra_headers: dict[str, str] | None = None + ) -> GetMarginSubtraderGroupsResponse: + """``GET /fcm/margin/subtrader_groups`` — list margin subtrader groups.""" + data = self._get("/fcm/margin/subtrader_groups", extra_headers=extra_headers) + return GetMarginSubtraderGroupsResponse.model_validate(data) + + def create_subtrader_group( + self, + *, + request: CreateMarginSubtraderGroupRequest | None = None, + subtrader_ids: list[str] | None = None, + extra_headers: dict[str, str] | None = None, + ) -> CreateMarginSubtraderGroupResponse: + """``POST /fcm/margin/subtrader_groups`` — create a netted subtrader group.""" + _check_request_exclusive(request, subtrader_ids=subtrader_ids) + if request is None: + if subtrader_ids is None: + raise TypeError( + "create_subtrader_group() requires `subtrader_ids` " + "(or pass `request=...`)" + ) + request = CreateMarginSubtraderGroupRequest(subtrader_ids=subtrader_ids) + data = self._post( + "/fcm/margin/subtrader_groups", + json=request.model_dump(exclude_none=True, by_alias=True, mode="json"), + extra_headers=extra_headers, + ) + return CreateMarginSubtraderGroupResponse.model_validate(data) + + def update_subtrader_group( + self, + group_id: str, + *, + request: UpdateMarginSubtraderGroupRequest | None = None, + subtrader_ids: list[str] | None = None, + extra_headers: dict[str, str] | None = None, + ) -> None: + """``PUT /fcm/margin/subtrader_groups/{group_id}`` — replace group membership.""" + _check_request_exclusive(request, subtrader_ids=subtrader_ids) + if request is None: + if subtrader_ids is None: + raise TypeError( + "update_subtrader_group() requires `subtrader_ids` " + "(or pass `request=...`)" + ) + request = UpdateMarginSubtraderGroupRequest(subtrader_ids=subtrader_ids) + self._put( + f"/fcm/margin/subtrader_groups/{_seg(group_id, name='group_id')}", + json=request.model_dump(exclude_none=True, by_alias=True, mode="json"), + extra_headers=extra_headers, + ) + + def delete_subtrader_group( + self, group_id: str, *, extra_headers: dict[str, str] | None = None + ) -> None: + """``DELETE /fcm/margin/subtrader_groups/{group_id}`` — delete a group.""" + self._delete( + f"/fcm/margin/subtrader_groups/{_seg(group_id, name='group_id')}", + extra_headers=extra_headers, + ) + class AsyncMarginResource(KlearAsyncResource): """Async Klear (SCM) margin API — all nine endpoints + two paginators.""" @@ -421,3 +493,65 @@ async def settlement_balance_withdrawal( extra_headers=extra_headers, ) return GetSettlementBalanceWithdrawalResponse.model_validate(data) + + async def list_subtrader_groups( + self, *, extra_headers: dict[str, str] | None = None + ) -> GetMarginSubtraderGroupsResponse: + """Async :meth:`MarginResource.list_subtrader_groups`.""" + data = await self._get("/fcm/margin/subtrader_groups", extra_headers=extra_headers) + return GetMarginSubtraderGroupsResponse.model_validate(data) + + async def create_subtrader_group( + self, + *, + request: CreateMarginSubtraderGroupRequest | None = None, + subtrader_ids: list[str] | None = None, + extra_headers: dict[str, str] | None = None, + ) -> CreateMarginSubtraderGroupResponse: + """Async :meth:`MarginResource.create_subtrader_group`.""" + _check_request_exclusive(request, subtrader_ids=subtrader_ids) + if request is None: + if subtrader_ids is None: + raise TypeError( + "create_subtrader_group() requires `subtrader_ids` " + "(or pass `request=...`)" + ) + request = CreateMarginSubtraderGroupRequest(subtrader_ids=subtrader_ids) + data = await self._post( + "/fcm/margin/subtrader_groups", + json=request.model_dump(exclude_none=True, by_alias=True, mode="json"), + extra_headers=extra_headers, + ) + return CreateMarginSubtraderGroupResponse.model_validate(data) + + async def update_subtrader_group( + self, + group_id: str, + *, + request: UpdateMarginSubtraderGroupRequest | None = None, + subtrader_ids: list[str] | None = None, + extra_headers: dict[str, str] | None = None, + ) -> None: + """Async :meth:`MarginResource.update_subtrader_group`.""" + _check_request_exclusive(request, subtrader_ids=subtrader_ids) + if request is None: + if subtrader_ids is None: + raise TypeError( + "update_subtrader_group() requires `subtrader_ids` " + "(or pass `request=...`)" + ) + request = UpdateMarginSubtraderGroupRequest(subtrader_ids=subtrader_ids) + await self._put( + f"/fcm/margin/subtrader_groups/{_seg(group_id, name='group_id')}", + json=request.model_dump(exclude_none=True, by_alias=True, mode="json"), + extra_headers=extra_headers, + ) + + async def delete_subtrader_group( + self, group_id: str, *, extra_headers: dict[str, str] | None = None + ) -> None: + """Async :meth:`MarginResource.delete_subtrader_group`.""" + await self._delete( + f"/fcm/margin/subtrader_groups/{_seg(group_id, name='group_id')}", + extra_headers=extra_headers, + ) diff --git a/kalshi/perps/models/fcm.py b/kalshi/perps/models/fcm.py new file mode 100644 index 00000000..19a0bb2d --- /dev/null +++ b/kalshi/perps/models/fcm.py @@ -0,0 +1,26 @@ +"""Perps FCM (futures commission merchant) models.""" + +from __future__ import annotations + +from pydantic import BaseModel, Field + + +class CreateMarginFCMSubtraderRequest(BaseModel): + """Body for POST /margin/fcm/subtraders. + + ``subtrader_suffix`` is the client-chosen suffix; the server composes the + full id as ``{user_id}_{subtrader_suffix}``. Spec pattern: 1-16 lowercase + alphanumeric characters. + """ + + subtrader_suffix: str = Field(min_length=1, max_length=16, pattern=r"^[a-z0-9]{1,16}$") + + model_config = {"extra": "forbid"} + + +class CreateMarginFCMSubtraderResponse(BaseModel): + """Response from POST /margin/fcm/subtraders.""" + + subtrader_id: str + + model_config = {"extra": "allow"} diff --git a/kalshi/perps/resources/fcm.py b/kalshi/perps/resources/fcm.py new file mode 100644 index 00000000..10068279 --- /dev/null +++ b/kalshi/perps/resources/fcm.py @@ -0,0 +1,100 @@ +"""Perps FCM resource — create margin FCM subtraders. + +``POST /margin/fcm/subtraders`` creates a new FCM subtrader under the +authenticated member. Auth required; POST is never retried. +""" + +from __future__ import annotations + +from typing import overload + +from kalshi.perps.models.fcm import ( + CreateMarginFCMSubtraderRequest, + CreateMarginFCMSubtraderResponse, +) +from kalshi.resources._base import ( + AsyncResource, + SyncResource, + _check_request_exclusive, +) + + +def _build_create_subtrader_body( + request: CreateMarginFCMSubtraderRequest | None, + *, + subtrader_suffix: str | None, +) -> dict[str, object]: + _check_request_exclusive(request, subtrader_suffix=subtrader_suffix) + if request is None: + if subtrader_suffix is None: + raise TypeError( + "create_subtrader() requires `subtrader_suffix` (or pass `request=...`)" + ) + request = CreateMarginFCMSubtraderRequest(subtrader_suffix=subtrader_suffix) + return request.model_dump(exclude_none=True, by_alias=True, mode="json") + + +class FcmResource(SyncResource): + """Sync perps FCM API.""" + + @overload + def create_subtrader( + self, + *, + request: CreateMarginFCMSubtraderRequest, + extra_headers: dict[str, str] | None = None, + ) -> CreateMarginFCMSubtraderResponse: ... + @overload + def create_subtrader( + self, + *, + subtrader_suffix: str, + extra_headers: dict[str, str] | None = None, + ) -> CreateMarginFCMSubtraderResponse: ... + def create_subtrader( + self, + *, + request: CreateMarginFCMSubtraderRequest | None = None, + subtrader_suffix: str | None = None, + extra_headers: dict[str, str] | None = None, + ) -> CreateMarginFCMSubtraderResponse: + """``POST /margin/fcm/subtraders`` — create a margin FCM subtrader. + + The full ``subtrader_id`` is composed server-side as + ``{user_id}_{subtrader_suffix}``. + """ + self._require_auth() + body = _build_create_subtrader_body(request, subtrader_suffix=subtrader_suffix) + data = self._post("/margin/fcm/subtraders", json=body, extra_headers=extra_headers) + return CreateMarginFCMSubtraderResponse.model_validate(data) + + +class AsyncFcmResource(AsyncResource): + """Async perps FCM API.""" + + @overload + async def create_subtrader( + self, + *, + request: CreateMarginFCMSubtraderRequest, + extra_headers: dict[str, str] | None = None, + ) -> CreateMarginFCMSubtraderResponse: ... + @overload + async def create_subtrader( + self, + *, + subtrader_suffix: str, + extra_headers: dict[str, str] | None = None, + ) -> CreateMarginFCMSubtraderResponse: ... + async def create_subtrader( + self, + *, + request: CreateMarginFCMSubtraderRequest | None = None, + subtrader_suffix: str | None = None, + extra_headers: dict[str, str] | None = None, + ) -> CreateMarginFCMSubtraderResponse: + """Async :meth:`FcmResource.create_subtrader`.""" + self._require_auth() + body = _build_create_subtrader_body(request, subtrader_suffix=subtrader_suffix) + data = await self._post("/margin/fcm/subtraders", json=body, extra_headers=extra_headers) + return CreateMarginFCMSubtraderResponse.model_validate(data) diff --git a/kalshi/resources/live_data.py b/kalshi/resources/live_data.py index fc4ebf34..cb7335a4 100644 --- a/kalshi/resources/live_data.py +++ b/kalshi/resources/live_data.py @@ -10,6 +10,8 @@ import builtins from kalshi.models.live_data import ( + EventLiveData, + GetEventLiveDataResponse, GetGameStatsResponse, GetLiveDataResponse, GetLiveDatasResponse, @@ -40,6 +42,27 @@ def get( ) return GetLiveDataResponse.model_validate(data).live_data + def get_event( + self, + event_ticker: str, + *, + range: str | None = None, + extra_headers: dict[str, str] | None = None, + ) -> EventLiveData: + """``GET /live_data/events/{event_ticker}`` — event-keyed live data. + + Serves crypto price charts, commodity timeseries, weather observations, + and similar event-scoped payloads. Optional ``range`` is a chart-window + hint (e.g. ``15min``, ``1h``, ``1d``) when the underlying type supports it. + """ + params = _params(range=range) + data = self._get( + f"/live_data/events/{_seg(event_ticker, name='event_ticker')}", + params=params, + extra_headers=extra_headers, + ) + return GetEventLiveDataResponse.model_validate(data).live_data + def get_typed( self, milestone_type: str, @@ -134,6 +157,25 @@ async def get( ) return GetLiveDataResponse.model_validate(data).live_data + async def get_event( + self, + event_ticker: str, + *, + range: str | None = None, + extra_headers: dict[str, str] | None = None, + ) -> EventLiveData: + """``GET /live_data/events/{event_ticker}`` — event-keyed live data. + + Async counterpart of :meth:`LiveDataResource.get_event`. + """ + params = _params(range=range) + data = await self._get( + f"/live_data/events/{_seg(event_ticker, name='event_ticker')}", + params=params, + extra_headers=extra_headers, + ) + return GetEventLiveDataResponse.model_validate(data).live_data + async def get_typed( self, milestone_type: str, diff --git a/kalshi/resources/subaccounts.py b/kalshi/resources/subaccounts.py index 4cdf6be3..b23ea755 100644 --- a/kalshi/resources/subaccounts.py +++ b/kalshi/resources/subaccounts.py @@ -3,14 +3,11 @@ from __future__ import annotations from collections.abc import AsyncIterator, Iterator -from decimal import Decimal -from typing import Any, Literal, overload +from typing import Any, overload from uuid import UUID from kalshi.models.common import Page from kalshi.models.subaccounts import ( - ApplySubaccountPositionTransferRequest, - ApplySubaccountPositionTransferResponse, ApplySubaccountTransferRequest, CreateSubaccountRequest, CreateSubaccountResponse, @@ -71,59 +68,6 @@ def _build_transfer_body( return request.model_dump(exclude_none=True, by_alias=True, mode="json") -def _build_position_transfer_body( - request: ApplySubaccountPositionTransferRequest | None, - *, - client_transfer_id: UUID | str | None, - from_subaccount: int | None, - to_subaccount: int | None, - market_ticker: str | None, - side: Literal["yes", "no"] | None, - count: int | None, - price: Decimal | None, -) -> dict[str, Any]: - _check_request_exclusive( - request, - client_transfer_id=client_transfer_id, - from_subaccount=from_subaccount, - to_subaccount=to_subaccount, - market_ticker=market_ticker, - side=side, - count=count, - price=price, - ) - if request is None: - if ( - client_transfer_id is None - or from_subaccount is None - or to_subaccount is None - or market_ticker is None - or side is None - or count is None - or price is None - ): - raise TypeError( - "transfer_position() requires `client_transfer_id`, `from_subaccount`, " - "`to_subaccount`, `market_ticker`, `side`, `count`, and `price` " - "(or pass `request=...`)" - ) - # Accept str for caller ergonomics; coerce once to surface a clean - # ValueError on malformed strings before the model validator sees them. - uid = ( - client_transfer_id if isinstance(client_transfer_id, UUID) else UUID(client_transfer_id) - ) - request = ApplySubaccountPositionTransferRequest( - client_transfer_id=uid, - from_subaccount=from_subaccount, - to_subaccount=to_subaccount, - market_ticker=market_ticker, - side=side, - count=count, - price=price, - ) - return request.model_dump(exclude_none=True, by_alias=True, mode="json") - - def _build_update_netting_body( request: UpdateSubaccountNettingRequest | None, *, @@ -212,63 +156,6 @@ def transfer( ) self._post("/portfolio/subaccounts/transfer", json=body, extra_headers=extra_headers) - @overload - def transfer_position( - self, - *, - request: ApplySubaccountPositionTransferRequest, - extra_headers: dict[str, str] | None = None, - ) -> ApplySubaccountPositionTransferResponse: ... - @overload - def transfer_position( - self, - *, - client_transfer_id: UUID | str, - from_subaccount: int, - to_subaccount: int, - market_ticker: str, - side: Literal["yes", "no"], - count: int, - price: Decimal, - extra_headers: dict[str, str] | None = None, - ) -> ApplySubaccountPositionTransferResponse: ... - def transfer_position( - self, - *, - request: ApplySubaccountPositionTransferRequest | None = None, - client_transfer_id: UUID | str | None = None, - from_subaccount: int | None = None, - to_subaccount: int | None = None, - market_ticker: str | None = None, - side: Literal["yes", "no"] | None = None, - count: int | None = None, - price: Decimal | None = None, - extra_headers: dict[str, str] | None = None, - ) -> ApplySubaccountPositionTransferResponse: - """Move an open position between subaccounts (spec v3.24.0). - - Unlike the cash-only :meth:`transfer`, this moves ``count`` contracts of - ``market_ticker`` (``side``) and returns the server-generated - ``position_transfer_id``. ``price`` is the per-contract cost basis in - fixed-point dollars (0-1.0) — pass a ``Decimal``, e.g. ``Decimal("0.50")``. - Spec v3.24.0 renamed this ``price_cents`` (integer cents) → ``price``. - """ - self._require_auth() - body = _build_position_transfer_body( - request, - client_transfer_id=client_transfer_id, - from_subaccount=from_subaccount, - to_subaccount=to_subaccount, - market_ticker=market_ticker, - side=side, - count=count, - price=price, - ) - data = self._post( - "/portfolio/subaccounts/positions/transfer", json=body, extra_headers=extra_headers - ) - return ApplySubaccountPositionTransferResponse.model_validate(data) - def list_balances( self, *, extra_headers: dict[str, str] | None = None ) -> GetSubaccountBalancesResponse: @@ -405,59 +292,6 @@ async def transfer( ) await self._post("/portfolio/subaccounts/transfer", json=body, extra_headers=extra_headers) - @overload - async def transfer_position( - self, - *, - request: ApplySubaccountPositionTransferRequest, - extra_headers: dict[str, str] | None = None, - ) -> ApplySubaccountPositionTransferResponse: ... - @overload - async def transfer_position( - self, - *, - client_transfer_id: UUID | str, - from_subaccount: int, - to_subaccount: int, - market_ticker: str, - side: Literal["yes", "no"], - count: int, - price: Decimal, - extra_headers: dict[str, str] | None = None, - ) -> ApplySubaccountPositionTransferResponse: ... - async def transfer_position( - self, - *, - request: ApplySubaccountPositionTransferRequest | None = None, - client_transfer_id: UUID | str | None = None, - from_subaccount: int | None = None, - to_subaccount: int | None = None, - market_ticker: str | None = None, - side: Literal["yes", "no"] | None = None, - count: int | None = None, - price: Decimal | None = None, - extra_headers: dict[str, str] | None = None, - ) -> ApplySubaccountPositionTransferResponse: - """Move an open position between subaccounts (spec v3.23.0). - - Async counterpart of :meth:`SubaccountsResource.transfer_position`. - """ - self._require_auth() - body = _build_position_transfer_body( - request, - client_transfer_id=client_transfer_id, - from_subaccount=from_subaccount, - to_subaccount=to_subaccount, - market_ticker=market_ticker, - side=side, - count=count, - price=price, - ) - data = await self._post( - "/portfolio/subaccounts/positions/transfer", json=body, extra_headers=extra_headers - ) - return ApplySubaccountPositionTransferResponse.model_validate(data) - async def list_balances( self, *, extra_headers: dict[str, str] | None = None ) -> GetSubaccountBalancesResponse: diff --git a/kalshi/ws/models/market_lifecycle.py b/kalshi/ws/models/market_lifecycle.py index 4e87c59c..23b683b7 100644 --- a/kalshi/ws/models/market_lifecycle.py +++ b/kalshi/ws/models/market_lifecycle.py @@ -55,6 +55,9 @@ class MarketLifecyclePayload(BaseModel): # list[dict] to match Market.price_ranges (no nested model yet). price_ranges: list[dict[str, Any]] | None = None yes_sub_title: str | None = None + # OpenAPI/AsyncAPI content 3.27.0: optional on `created` lifecycle events — + # exchange shard the market lives on. + exchange_index: int | None = None model_config = {"extra": "allow", "populate_by_name": True} diff --git a/pyproject.toml b/pyproject.toml index 81225b32..43875b8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kalshi-sdk" -version = "8.0.0" +version = "9.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/asyncapi.yaml b/specs/asyncapi.yaml index 8bcfa6e9..13704eac 100644 --- a/specs/asyncapi.yaml +++ b/specs/asyncapi.yaml @@ -1833,6 +1833,7 @@ components: msg: market_ticker: INXD-23SEP14-B4487 event_type: created + exchange_index: 0 open_ts: 1694635200 close_ts: 1694721600 price_level_structure: linear_cent @@ -1899,6 +1900,7 @@ components: msg: market_ticker: KXMVE-TEST-EVENT-M1 event_type: created + exchange_index: 0 open_ts: 1773936000 close_ts: 1774022400 additional_metadata: @@ -1926,6 +1928,7 @@ components: sid: 5 msg: event_ticker: KXQUICKSETTLE-26JAN25H2150 + exchange_index: 0 title: What will 1+1 equal on Jan 25 at 21:50? subtitle: Jan 25 at 21:50 collateral_return_type: MECNET @@ -3412,6 +3415,11 @@ components: description: >- Unique identifier for markets. This is what you use to differentiate updates for different markets + exchange_index: + type: integer + description: >- + Optional - This key will ONLY exist when the market is created. + Identifier for the exchange shard the market lives on open_ts: type: integer description: >- @@ -3581,6 +3589,7 @@ components: type: object required: - event_ticker + - exchange_index - title - subtitle - collateral_return_type @@ -3589,6 +3598,9 @@ components: event_ticker: type: string description: Unique identifier for the event being created + exchange_index: + type: integer + description: Identifier for the exchange shard the event's markets live on title: type: string description: Title of event diff --git a/specs/openapi.yaml b/specs/openapi.yaml index 0144ee1e..67dfa3ae 100644 --- a/specs/openapi.yaml +++ b/specs/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.0.0 info: title: Kalshi Trade API Manual Endpoints - version: 3.26.0 + version: 3.27.0 description: >- Manually defined OpenAPI spec for endpoints being migrated to spec-first approach @@ -1578,6 +1578,7 @@ paths: kalshiAccessTimestamp: [] parameters: - $ref: '#/components/parameters/OrderGroupIdPath' + - $ref: '#/components/parameters/SubaccountQueryDefaultPrimary' - $ref: '#/components/parameters/ExchangeIndexQuery' requestBody: required: true @@ -1632,9 +1633,7 @@ paths: post: operationId: IntraExchangeInstanceTransfer summary: Intra Account Transfer - description: >- - Endpoint for transferring funds within the same account. This endpoint - is currently not available. + description: Endpoint for transferring funds within the same account. tags: - portfolio security: @@ -1730,47 +1729,6 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' - /portfolio/subaccounts/positions/transfer: - post: - x-excluded: true - operationId: ApplySubaccountPositionTransfer - summary: Transfer Position Between Subaccounts - description: >- - Moves an existing position between two of the authenticated user's own - subaccounts. Use 0 for the primary account, or 1-63 for numbered - subaccounts. The transfer is idempotent on `client_transfer_id`: - retrying with the same value returns 409. `price` is the per-contract - transfer price as a fixed-point dollar string, and is always the - YES-side price regardless of `side` — the receiving subaccount pays the - sending subaccount for the position at that price. See the - [Subaccounts](/getting_started/subaccounts) page for worked examples. - tags: - - portfolio - security: - - kalshiAccessKey: [] - kalshiAccessSignature: [] - kalshiAccessTimestamp: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ApplySubaccountPositionTransferRequest' - responses: - '200': - description: Position transfer completed successfully - content: - application/json: - schema: - $ref: '#/components/schemas/ApplySubaccountPositionTransferResponse' - '400': - $ref: '#/components/responses/BadRequestError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '409': - $ref: '#/components/responses/ConflictError' - '500': - $ref: '#/components/responses/InternalServerError' /portfolio/subaccounts/balances: get: operationId: GetSubaccountBalances @@ -3177,6 +3135,44 @@ paths: description: Game stats not found '500': description: Internal server error + /live_data/events/{event_ticker}: + get: + operationId: GetEventLiveData + summary: Get Event Live Data + description: >- + Get live data for an event by its event ticker. Serves event-keyed live + data such as crypto price charts, commodity price timeseries, and + weather observations. The `type` field in the response names the schema + of the `details` object. + tags: + - live-data + parameters: + - name: event_ticker + in: path + required: true + description: Event ticker + schema: + type: string + - name: range + in: query + required: false + description: >- + Optional chart range hint (e.g. `15min`, `1h`, `1d`). When the + underlying live data type supports it, restricts the returned + timeseries to the requested window. + schema: + type: string + responses: + '200': + description: Live data retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GetEventLiveDataResponse' + '404': + description: Live data not found + '500': + description: Internal server error /structured_targets: get: operationId: GetStructuredTargets @@ -4456,9 +4452,7 @@ components: 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. example: 0 FeeType: type: string @@ -4616,9 +4610,6 @@ components: details: type: string description: Additional details about the error, if available - service: - type: string - description: The name of the service that generated the error SelfTradePreventionType: type: string enum: @@ -5464,6 +5455,43 @@ components: type: array items: $ref: '#/components/schemas/LiveData' + EventLiveData: + type: object + required: + - type + - details + properties: + type: + type: string + description: Type of live data. Names the schema of the details object. + details: + type: object + additionalProperties: true + description: >- + Live data details as a flexible object whose shape depends on the + type. + is_historical: + type: boolean + description: >- + Present for crypto live data. True when the event has matured and + the payload is a frozen historical snapshot. + default_range: + type: string + description: >- + Chart range the client should default to (e.g. `15min`, `1h`). + Omitted when unset. + range_options: + type: array + items: + type: string + description: Chart range menu options. Omitted when unset. + GetEventLiveDataResponse: + type: object + required: + - live_data + properties: + live_data: + $ref: '#/components/schemas/EventLiveData' GetGameStatsResponse: type: object properties: @@ -5583,79 +5611,6 @@ components: ApplySubaccountTransferResponse: type: object description: Empty response indicating successful transfer. - ApplySubaccountPositionTransferRequest: - type: object - required: - - client_transfer_id - - from_subaccount - - to_subaccount - - market_ticker - - side - - count - - price - properties: - client_transfer_id: - type: string - format: uuid - description: >- - Unique client-provided transfer ID for idempotency. Retrying with - the same value returns 409. - x-oapi-codegen-extra-tags: - validate: required - from_subaccount: - type: integer - nullable: true - description: >- - Source subaccount number (0 for primary, 1-63 for numbered - subaccounts). - x-oapi-codegen-extra-tags: - validate: required - to_subaccount: - type: integer - nullable: true - description: >- - Destination subaccount number (0 for primary, 1-63 for numbered - subaccounts). - x-oapi-codegen-extra-tags: - validate: required - market_ticker: - type: string - description: >- - Ticker of the market whose position is being moved. The market must - be on exchange shard 0; markets on any other shard are rejected. - x-oapi-codegen-extra-tags: - validate: required - side: - type: string - enum: - - 'yes' - - 'no' - description: Side of the position to move. - x-oapi-codegen-extra-tags: - validate: required - count: - type: integer - nullable: true - description: Number of contracts to move (must be greater than 0). - x-oapi-codegen-extra-tags: - validate: required - price: - $ref: '#/components/schemas/FixedPointDollars' - description: >- - Per-contract price in fixed-point dollars (0 to 1.00 inclusive): the - cash consideration the receiving subaccount pays the sending - subaccount for the position. Always the YES-side price, even when - `side` is `no`; a NO position transferred at `price` p carries a - per-contract NO-side value of 1.00 − p. - x-go-type-skip-optional-pointer: true - ApplySubaccountPositionTransferResponse: - type: object - required: - - position_transfer_id - properties: - position_transfer_id: - type: string - description: Server-generated identifier for the position transfer. GetSubaccountBalancesResponse: type: object required: @@ -6821,9 +6776,8 @@ components: minimum: 0 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 + primary, 1-63 for subaccounts). Subaccount-restricted API keys must + omit this field or pass their locked subaccount. contracts_limit: type: integer format: int64 @@ -7819,6 +7773,9 @@ 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 AmendOrderResponse: type: object @@ -7931,11 +7888,10 @@ components: subaccount: type: integer minimum: 0 - default: 0 description: >- The subaccount number to use for this order. 0 is the primary - subaccount. - x-go-type-skip-optional-pointer: true + subaccount. Subaccount-restricted API keys must omit this field or + pass their locked subaccount. order_group_id: type: string description: The order group this order is part of @@ -8040,6 +7996,10 @@ components: - $ref: '#/components/schemas/ExchangeIndex' default: 0 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 DecreaseOrderV2Response: type: object required: @@ -8114,6 +8074,9 @@ 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 AmendOrderV2Response: type: object @@ -8292,11 +8255,10 @@ components: subaccount: type: integer minimum: 0 - default: 0 description: >- Optional subaccount number to use for this cancellation (0 for - primary, 1-63 for subaccounts). - x-go-type-skip-optional-pointer: true + primary, 1-63 for subaccounts). Subaccount-restricted API keys + must omit this field or pass their locked subaccount. exchange_index: allOf: - $ref: '#/components/schemas/ExchangeIndex' @@ -9047,6 +9009,7 @@ components: calculations. additional_prohibitions: type: array + nullable: true items: type: string description: >- @@ -9061,6 +9024,11 @@ components: type: string format: date-time description: Timestamp of when this series' metadata was last updated. + exchange_index: + allOf: + - $ref: '#/components/schemas/ExchangeIndex' + x-go-type-skip-optional-pointer: true + x-omitempty: false SeriesFeeChange: type: object required: diff --git a/specs/perps_openapi.yaml b/specs/perps_openapi.yaml index 4ab23d15..7a47cf72 100644 --- a/specs/perps_openapi.yaml +++ b/specs/perps_openapi.yaml @@ -2,15 +2,50 @@ openapi: 3.0.0 info: title: Kalshi Trade API Manual Endpoints version: 0.0.1 - description: >- - Manually defined OpenAPI spec for endpoints being migrated to spec-first - approach + 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 perps REST API server - url: https://external-api.demo.kalshi.co/trade-api/v2 description: Demo perps REST API server + paths: + /margin/fcm/subtraders: + post: + operationId: CreateMarginFCMSubtrader + summary: Create Margin FCM Subtrader + description: Endpoint for FCM members to create a margin subtrader. + tags: + - fcm + security: + - kalshiAccessKey: [] + kalshiAccessSignature: [] + kalshiAccessTimestamp: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateMarginFCMSubtraderRequest' + responses: + '201': + description: Subtrader created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/CreateMarginFCMSubtraderResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + $ref: '#/components/responses/ForbiddenError' + '409': + $ref: '#/components/responses/ConflictError' + '500': + $ref: '#/components/responses/InternalServerError' + /account/limits/perps: get: operationId: GetPerpsAccountApiLimits @@ -33,11 +68,12 @@ paths: description: Unauthorized '500': description: Internal server error + /margin/exchange/status: get: operationId: GetMarginExchangeStatus summary: Get Exchange Status - description: Endpoint for getting the margin exchange status. + description: 'Endpoint for getting the margin exchange status.' tags: - exchange responses: @@ -65,13 +101,12 @@ paths: application/json: schema: $ref: '#/components/schemas/ExchangeStatus' + /margin/risk_parameters: get: operationId: GetMarginRiskParameters summary: Get Risk Parameters - description: >- - Returns system-wide margin risk parameters including liquidation - thresholds and per-market initial margin multipliers. + description: 'Returns system-wide margin risk parameters including liquidation thresholds and per-market initial margin multipliers.' tags: - risk responses: @@ -81,11 +116,12 @@ paths: application/json: schema: $ref: '#/components/schemas/GetMarginRiskParametersResponse' + /margin/orders: get: operationId: GetMarginOrders summary: Get Orders - description: Endpoint for listing margin orders with optional filtering. + description: 'Endpoint for listing margin orders with optional filtering.' tags: - orders security: @@ -146,6 +182,7 @@ paths: $ref: '#/components/responses/RateLimitError' '500': $ref: '#/components/responses/InternalServerError' + /margin/orders/{order_id}: get: operationId: GetMarginOrder @@ -172,12 +209,11 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' + delete: operationId: CancelMarginOrder summary: Cancel Order - description: >- - Endpoint for canceling an order. Cancels all remaining resting contracts - and returns the canceled order details. + description: Endpoint for canceling an order. Cancels all remaining resting contracts and returns the canceled order details. tags: - orders security: @@ -200,14 +236,12 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' + /margin/orders/{order_id}/decrease: post: operationId: DecreaseMarginOrder summary: Decrease Order - description: >- - Endpoint for decreasing the number of contracts in an existing order. - Exactly one of `reduce_by` or `reduce_to` must be provided. Canceling an - order is equivalent to decreasing to zero. + description: Endpoint for decreasing the number of contracts in an existing order. Exactly one of `reduce_by` or `reduce_to` must be provided. Canceling an order is equivalent to decreasing to zero. tags: - orders security: @@ -238,22 +272,16 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' + /margin/orders/{order_id}/amend: post: operationId: AmendMarginOrder summary: Amend Order - description: >- - Endpoint for amending the price and/or max number of fillable contracts - in an existing margin order. + description: Endpoint for amending the price and/or max number of fillable contracts in an existing margin order. 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 @@ -285,6 +313,7 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' + /margin/markets: get: operationId: GetMarginMarkets @@ -313,13 +342,12 @@ paths: $ref: '#/components/responses/RateLimitError' '500': $ref: '#/components/responses/InternalServerError' + /margin/markets/{ticker}: get: operationId: GetMarginMarket summary: Get Market - description: >- - Endpoint for fetching a margin market with trading stats (price, volume, - open interest). + description: Endpoint for fetching a margin market with trading stats (price, volume, open interest). tags: - market parameters: @@ -344,6 +372,7 @@ paths: $ref: '#/components/responses/RateLimitError' '500': $ref: '#/components/responses/InternalServerError' + /margin/markets/{ticker}/orderbook: get: operationId: GetMarginMarketOrderbook @@ -368,9 +397,7 @@ paths: default: 0 - name: aggregation_tick_size in: query - description: >- - Tick size in dollars for aggregating price levels (e.g., 0.10 for 10 - cent buckets) + description: Tick size in dollars for aggregating price levels (e.g., 0.10 for 10 cent buckets) required: false schema: type: string @@ -389,6 +416,7 @@ paths: $ref: '#/components/responses/RateLimitError' '500': $ref: '#/components/responses/InternalServerError' + /margin/markets/{ticker}/candlesticks: get: operationId: GetMarginMarketCandlesticks @@ -406,49 +434,34 @@ 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 `price.previous` 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 `price.previous` to the close price from the real candlestick schema: type: boolean default: false @@ -465,6 +478,7 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' + /margin/fills: get: operationId: GetMarginFills @@ -528,11 +542,12 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' + /margin/positions: get: operationId: GetMarginPositions summary: Get Positions - description: Endpoint for retrieving the authenticated user's margin positions. + description: 'Endpoint for retrieving the authenticated user''s margin positions.' tags: - portfolio security: @@ -566,14 +581,12 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' + /margin/trades: get: operationId: GetMarginTrades summary: Get Trades - description: >- - Endpoint for retrieving public margin trades for a given market ticker. - Returns a paginated response. Use the cursor value from the previous - response to get the next page. + description: 'Endpoint for retrieving public margin trades for a given market ticker. Returns a paginated response. Use the cursor value from the previous response to get the next page.' tags: - market parameters: @@ -625,13 +638,12 @@ paths: $ref: '#/components/responses/BadRequestError' '500': $ref: '#/components/responses/InternalServerError' + /margin/enabled: get: operationId: GetMarginEnabled summary: Get Enabled Status - description: >- - Endpoint for checking if margin trading is enabled for the authenticated - user. + description: Endpoint for checking if margin trading is enabled for the authenticated user. tags: - exchange security: @@ -649,13 +661,12 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' + /margin/notional_risk_limit: get: operationId: GetMarginNotionalRiskLimit summary: Get Notional Risk Limit - description: >- - Endpoint for retrieving the notional value risk limit for the - authenticated margin user. + description: 'Endpoint for retrieving the notional value risk limit for the authenticated margin user.' tags: - risk security: @@ -673,24 +684,16 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' + /margin/balance: get: operationId: GetMarginBalance summary: Get Balance - description: >- - Endpoint for retrieving the balance breakdown for the authenticated - direct margin user. Returns cash balance (aggregate and per-subaccount), - position value, total balance, and maintenance margin requirement. + description: 'Endpoint for retrieving the balance breakdown for the authenticated direct margin user. Returns cash balance (aggregate and per-subaccount), position value, total balance, and maintenance margin requirement.' x-mint: - content: > + content: | - - **Rate limit:** 5 tokens per request, or 50 tokens when - `compute_available_balance=true` (the available-balance computation - scans all resting orders). See `GET - /trade-api/v2/account/endpoint_costs` for current non-default endpoint - costs. - + **Rate limit:** 5 tokens per request, or 50 tokens when `compute_available_balance=true` (the available-balance computation scans all resting orders). See `GET /trade-api/v2/account/endpoint_costs` for current non-default endpoint costs. tags: - portfolio @@ -706,10 +709,7 @@ paths: type: boolean default: false x-go-type-skip-optional-pointer: true - description: >- - When true, computes available_balance per subaccount at an increased - rate limit cost. Available balance is 0 when the flag is false or - omitted. + description: 'When true, computes available_balance per subaccount at an increased rate limit cost. Available balance is 0 when the flag is false or omitted.' responses: '200': description: Margin balance retrieved successfully @@ -725,15 +725,12 @@ paths: $ref: '#/components/responses/RateLimitError' '500': $ref: '#/components/responses/InternalServerError' + /margin/risk: get: operationId: GetMarginRisk summary: Get Risk - description: >- - Endpoint for retrieving leverage and liquidation price data for the - authenticated direct margin user. Returns account-level leverage plus - per-position leverage and liquidation prices, grouped by subaccount and - market. + description: 'Endpoint for retrieving leverage and liquidation price data for the authenticated direct margin user. Returns account-level leverage plus per-position leverage and liquidation prices, grouped by subaccount and market.' tags: - risk security: @@ -753,14 +750,12 @@ paths: $ref: '#/components/responses/ForbiddenError' '500': $ref: '#/components/responses/InternalServerError' + /margin/fee_tiers: get: operationId: GetMarginFeeTiers summary: Get Fee Tiers - description: >- - Endpoint for retrieving the margin fee tiers for the authenticated - direct margin user. Returns a map of margin market tickers to their fee - tier strings. + description: 'Endpoint for retrieving the margin fee tiers for the authenticated direct margin user. Returns a map of margin market tickers to their fee tier strings.' tags: - fees security: @@ -778,15 +773,12 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' + /margin/funding_history: get: operationId: GetMarginFundingHistory summary: Get Funding History - description: >- - Endpoint for retrieving the authenticated user's historical margin - funding payments joined with funding rates for a specific market, or - across all markets when ticker is empty, over an inclusive UTC date - range. + description: 'Endpoint for retrieving the authenticated user''s historical margin funding payments joined with funding rates for a specific market, or across all markets when ticker is empty, over an inclusive UTC date range.' tags: - funding security: @@ -797,18 +789,14 @@ paths: - name: ticker in: query required: false - description: >- - Market ticker for funding history. Leave empty to query across all - markets. + description: Market ticker for funding history. Leave empty to query across all markets. schema: type: string x-go-type-skip-optional-pointer: true - name: start_date in: query required: true - description: >- - Inclusive UTC start date for funding history range (YYYY-MM-DD - format) + description: Inclusive UTC start date for funding history range (YYYY-MM-DD format) schema: type: string format: date @@ -841,13 +829,12 @@ paths: $ref: '#/components/responses/ForbiddenError' '500': $ref: '#/components/responses/InternalServerError' + /margin/funding_rates/historical: get: operationId: GetMarginHistoricalFundingRates summary: Get Historical Funding Rates - description: >- - Endpoint for retrieving historical margin funding rates for a market, or - across all markets when ticker is empty. + description: Endpoint for retrieving historical margin funding rates for a market, or across all markets when ticker is empty. tags: - funding parameters: @@ -861,18 +848,14 @@ paths: - name: start_ts in: query required: false - description: >- - Start timestamp (Unix timestamp in seconds). If omitted, defaults to - the earliest available data. + description: Start timestamp (Unix timestamp in seconds). If omitted, defaults to the earliest available data. schema: type: integer format: int64 - name: end_ts in: query required: false - description: >- - End timestamp (Unix timestamp in seconds). If omitted, defaults to - the current time. + description: End timestamp (Unix timestamp in seconds). If omitted, defaults to the current time. schema: type: integer format: int64 @@ -887,16 +870,13 @@ paths: $ref: '#/components/responses/BadRequestError' '500': $ref: '#/components/responses/InternalServerError' + /margin/funding_rates/estimate: get: operationId: GetMarginFundingRateEstimate summary: Get Funding Rate Estimate - description: > - Returns the estimated funding rate for the current, in-progress funding - period. The value is a time-weighted average of the premium index - computed over `[last_funding_time, now)`, so it continues to move as new - data accumulates through the window and is only finalized at - `next_funding_time`. + description: | + Returns the estimated funding rate for the current, in-progress funding period. The value is a time-weighted average of the premium index computed over `[last_funding_time, now)`, so it continues to move as new data accumulates through the window and is only finalized at `next_funding_time`. tags: - funding parameters: @@ -920,11 +900,12 @@ paths: $ref: '#/components/responses/BadRequestError' '500': $ref: '#/components/responses/InternalServerError' + /portfolio/intra_exchange_instance_transfer: post: operationId: IntraExchangeInstanceTransfer summary: Intra Account Transfer - description: ' Endpoint for transferring funds within the same account. This endpoint is currently not available.' + description: 'Endpoint for transferring funds within the same account.' tags: - portfolio security: @@ -952,14 +933,12 @@ paths: $ref: '#/components/responses/ForbiddenError' '500': $ref: '#/components/responses/InternalServerError' + /portfolio/margin/subaccounts: post: operationId: CreateMarginSubaccount summary: Create Subaccount - description: >- - Creates a new subaccount for the authenticated user in the margin - exchange. 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 in the margin exchange. Subaccounts are numbered sequentially starting from 1. Maximum 63 numbered subaccounts per user (64 including the primary account).' tags: - portfolio security: @@ -981,13 +960,12 @@ paths: $ref: '#/components/responses/ForbiddenError' '500': $ref: '#/components/responses/InternalServerError' + /portfolio/margin/subaccounts/transfer: post: operationId: ApplyMarginSubaccountTransfer summary: Transfer Between Subaccounts - description: >- - Transfers funds between the authenticated user's margin subaccounts. Use - 0 for the primary account, or 1-63 for numbered subaccounts. + description: 'Transfers funds between the authenticated user''s margin subaccounts. Use 0 for the primary account, or 1-63 for numbered subaccounts.' tags: - portfolio security: @@ -1013,13 +991,12 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' + /margin/order_groups: get: operationId: GetMarginOrderGroups summary: Get Order Groups - description: >- - Retrieves all order groups for the authenticated user on the margin - exchange. + description: 'Retrieves all order groups for the authenticated user on the margin exchange.' tags: - order-groups security: @@ -1041,14 +1018,12 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' + /margin/order_groups/create: post: operationId: CreateMarginOrderGroup summary: Create Order Group - description: >- - Creates a new order group on the margin exchange with a contracts limit - measured over a rolling window. When the limit is hit, all orders in the - group are cancelled and no new orders can be placed until reset. + description: 'Creates a new order group on the margin exchange with a contracts limit measured over a rolling window. When the limit is hit, all orders in the group are cancelled and no new orders can be placed until reset.' tags: - order-groups security: @@ -1074,13 +1049,12 @@ paths: $ref: '#/components/responses/UnauthorizedError' '500': $ref: '#/components/responses/InternalServerError' + /margin/order_groups/{order_group_id}: get: operationId: GetMarginOrderGroup summary: Get Order Group - description: >- - Retrieves details for a single order group on the margin exchange - including all order IDs and auto-cancel status. + description: 'Retrieves details for a single order group on the margin exchange including all order IDs and auto-cancel status.' tags: - order-groups security: @@ -1106,9 +1080,7 @@ paths: delete: operationId: DeleteMarginOrderGroup summary: Delete Order Group - description: >- - Deletes an order group on the margin exchange and cancels all orders - within it. + description: 'Deletes an order group on the margin exchange and cancels all orders within it.' tags: - order-groups security: @@ -1131,14 +1103,12 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' + /margin/order_groups/{order_group_id}/reset: put: operationId: ResetMarginOrderGroup summary: Reset Order Group - description: >- - Resets the order group matched contracts counter to zero on the margin - exchange, allowing new orders to be placed again after the limit was - hit. + description: 'Resets the order group matched contracts counter to zero on the margin exchange, allowing new orders to be placed again after the limit was hit.' tags: - order-groups security: @@ -1167,13 +1137,12 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' + /margin/order_groups/{order_group_id}/trigger: put: operationId: TriggerMarginOrderGroup summary: Trigger Order Group - description: >- - Triggers the order group on the margin exchange, canceling all orders in - the group and preventing new orders until the group is reset. + description: 'Triggers the order group on the margin exchange, canceling all orders in the group and preventing new orders until the group is reset.' tags: - order-groups security: @@ -1202,14 +1171,12 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' + /margin/order_groups/{order_group_id}/limit: put: operationId: UpdateMarginOrderGroupLimit summary: Update Order Group Limit - description: >- - Updates the order group contracts limit on the margin exchange. If the - updated limit would immediately trigger the group, all orders in the - group are canceled and the group is triggered. + description: 'Updates the order group contracts limit on the margin exchange. If the updated limit would immediately trigger the group, all orders in the group are canceled and the group is triggered.' tags: - order-groups security: @@ -1240,6 +1207,7 @@ paths: $ref: '#/components/responses/NotFoundError' '500': $ref: '#/components/responses/InternalServerError' + components: securitySchemes: kalshiAccessKey: @@ -1289,9 +1257,7 @@ 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: @@ -1303,6 +1269,25 @@ components: schema: $ref: '#/components/schemas/ErrorResponse' schemas: + CreateMarginFCMSubtraderRequest: + type: object + required: + - subtrader_suffix + properties: + subtrader_suffix: + type: string + pattern: '^[a-z0-9]{1,16}$' + description: Suffix for the new subtrader. The full subtrader id is composed server-side as {user_id}_{subtrader_suffix}. + + CreateMarginFCMSubtraderResponse: + type: object + required: + - subtrader_id + properties: + subtrader_id: + type: string + description: The full id of the created subtrader, in the form {user_id}_{subtrader_suffix}. + ApplySubaccountTransferRequest: type: object required: @@ -1316,17 +1301,13 @@ 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 @@ -1340,31 +1321,19 @@ components: subaccount: type: integer minimum: 0 - 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 + description: Optional subaccount number to use for this order group (0 for primary, 1-63 for subaccounts). Subaccount-restricted API keys must omit this field or pass their locked subaccount. contracts_limit: type: integer 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' @@ -1382,9 +1351,7 @@ 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: @@ -1398,6 +1365,7 @@ components: subaccount_number: type: integer description: The sequential number assigned to this subaccount (1-63). + # Order Group schemas EmptyResponse: type: object description: An empty response body @@ -1413,20 +1381,13 @@ components: details: type: string description: Additional details about the error, if available - service: - type: string - description: The name of the service that generated the error 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 ExchangeInstance: type: string - enum: - - event_contract - - margined + enum: ['event_contract', 'margined'] description: The exchange instance type BucketLimit: type: object @@ -1468,10 +1429,7 @@ components: $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: @@ -1490,31 +1448,19 @@ components: 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).' 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; the minimum granularity is - 0.01 contracts. - 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; the minimum granularity is 0.01 contracts. + example: "10.00" + # 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" GetOrderGroupResponse: type: object required: @@ -1526,9 +1472,7 @@ 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 @@ -1595,9 +1539,7 @@ 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 @@ -1607,31 +1549,20 @@ components: allOf: - $ref: '#/components/schemas/ExchangeIndex' x-go-type-skip-optional-pointer: true + # 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). 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. UpdateOrderGroupLimitRequest: type: object properties: @@ -1639,22 +1570,14 @@ 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. ExchangeStatus: type: object required: @@ -1663,14 +1586,11 @@ components: properties: exchange_active: type: boolean - description: >- - False if the exchange is no longer taking any state changes at all. - True unless under maintenance. + description: False if the exchange is no longer taking any state changes at all. True unless under maintenance. trading_active: type: boolean - description: >- - True if trading is currently permitted on the exchange. False - outside exchange hours or during pauses. + description: True if trading is currently permitted on the exchange. False outside exchange hours or during pauses. + GetMarginRiskParametersResponse: type: object required: @@ -1691,10 +1611,7 @@ components: additionalProperties: type: number format: double - description: >- - Map of market ticker to initial margin multiplier. The initial - margin requirement is the maintenance margin multiplied by this - value. + description: Map of market ticker to initial margin multiplier. The initial margin requirement is the maintenance margin multiplied by this value. CreateMarginOrderRequest: type: object required: @@ -1729,10 +1646,7 @@ components: format: int64 time_in_force: type: string - 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 @@ -1746,28 +1660,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. Orders with reduce_only set to true will - be rejected unless time_in_force is immediate_or_cancel or - fill_or_kill. + description: Specifies whether the order place count should be capped by the member's current position. Orders with reduce_only set to true will be rejected unless time_in_force is immediate_or_cancel or fill_or_kill. subaccount: type: integer minimum: 0 default: 0 - description: >- - The subaccount number to use for this margin order. 0 is the primary - subaccount. + description: The subaccount number to use for this margin order. 0 is the primary subaccount. x-go-type-skip-optional-pointer: true order_group_id: type: string description: The order group this order is part of x-go-type-skip-optional-pointer: true + CreateMarginOrderResponse: type: object required: @@ -1784,19 +1691,14 @@ 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. + GetMarginOrderResponse: type: object required: @@ -1804,6 +1706,7 @@ components: properties: order: $ref: '#/components/schemas/MarginOrder' + GetMarginOrdersResponse: type: object required: @@ -1816,6 +1719,7 @@ components: $ref: '#/components/schemas/MarginOrder' cursor: type: string + MarginOrder: type: object required: @@ -1872,20 +1776,17 @@ components: x-omitempty: false 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. order_group_id: type: string description: The order group this order is part of order_source: $ref: '#/components/schemas/OrderSource' - description: >- - The source of the order. Indicates whether the order was placed by - the user or by the system on behalf of the user. + description: The source of the order. Indicates whether the order was placed by the user or by the system on behalf of the user. order_reason: $ref: '#/components/schemas/OrderReason' description: The reason for a system-generated order, when applicable. + CancelMarginOrderResponse: type: object required: @@ -1898,24 +1799,20 @@ 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). + DecreaseMarginOrderRequest: type: object 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. + DecreaseMarginOrderResponse: type: object required: @@ -1929,6 +1826,7 @@ components: remaining_count: $ref: '#/components/schemas/FixedPointCount' description: Number of contracts remaining after the decrease. + AmendMarginOrderRequest: type: object required: @@ -1963,6 +1861,7 @@ components: type: string description: The new client-specified order ID after amendment x-go-type-skip-optional-pointer: true + AmendMarginOrderResponse: type: object required: @@ -1976,30 +1875,23 @@ components: $ref: '#/components/schemas/FixedPointCount' nullable: true x-omitempty: false - description: >- - Number of contracts remaining after the amend. Only present when the - amend caused a fill or changed the resting size. + description: Number of contracts remaining after the amend. 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. + MarginOrderbookCount: type: object required: @@ -2008,18 +1900,15 @@ components: properties: bids: type: array - description: >- - Bid price levels, ordered from best bid downward. Each level is - [price, quantity]. + description: Bid price levels, ordered from best bid downward. Each level is [price, quantity]. items: $ref: '#/components/schemas/PriceLevelDollarsCountFp' asks: type: array - description: >- - Ask price levels, ordered from best ask upward. Each level is - [price, quantity]. + description: Ask price levels, ordered from best ask upward. Each level is [price, quantity]. items: $ref: '#/components/schemas/PriceLevelDollarsCountFp' + MarginOrderbookResponse: type: object required: @@ -2027,6 +1916,7 @@ components: properties: orderbook: $ref: '#/components/schemas/MarginOrderbookCount' + MarginMarket: type: object required: @@ -2056,9 +1946,8 @@ components: type: number format: double description: > - Leverage estimate (1 / margin_rate) evaluated at a small - retail-sized notional position. Actual leverage may be lower for - larger positions as the liquidation margin rate grows with size. + Leverage estimate (1 / margin_rate) evaluated at a small retail-sized notional position. + Actual leverage may be lower for larger positions as the liquidation margin rate grows with size. Null when margin config or price data is unavailable. leverage_estimates: type: object @@ -2066,10 +1955,10 @@ components: type: number format: double description: > - Leverage estimates (1 / margin_rate) keyed by notional position size - in dollars ("1000", "10000", "100000", "1000000"). Leverage - decreases at larger notionals as the liquidation margin rate grows - with size. Null when margin config or price data is unavailable. + Leverage estimates (1 / margin_rate) keyed by notional position size in dollars + ("1000", "10000", "100000", "1000000"). Leverage decreases at larger notionals as + the liquidation margin rate grows with size. + Null when margin config or price data is unavailable. price: $ref: '#/components/schemas/FixedPointDollars' description: Last trade price in dollars. @@ -2090,9 +1979,7 @@ components: description: One sided trade volume in the last 24 hours. volume_24h_notional_value_dollars: $ref: '#/components/schemas/FixedPointDollars' - description: >- - Total notional value of one sided trade volume in the last 24 hours - in dollars. + description: Total notional value of one sided trade volume in the last 24 hours in dollars. bid: $ref: '#/components/schemas/FixedPointDollars' description: Best bid price in dollars. @@ -2110,6 +1997,7 @@ components: description: Underlying reference price, scaled per contract. schedule: $ref: '#/components/schemas/MarginMarketSchedule' + MarginMarketSchedule: type: object nullable: true @@ -2126,16 +2014,13 @@ components: type: integer format: int64 nullable: true - description: >- - Unix timestamp in seconds for the next scheduled close. Null while - closed. + description: Unix timestamp in seconds for the next scheduled close. Null while closed. next_open_ts: type: integer format: int64 nullable: true - description: >- - Unix timestamp in seconds for the next scheduled open. Null while - open. + description: Unix timestamp in seconds for the next scheduled open. Null while open. + TickerPrice: type: object required: @@ -2149,40 +2034,26 @@ components: type: integer format: int64 description: Source timestamp in epoch milliseconds. + MarginMarketStatus: type: string - enum: - - inactive - - active - - closed + enum: [inactive, active, closed] description: The status of a margin market + OrderSource: type: string - enum: - - user - - system - description: >- - The source of the order. 'user' indicates a user-placed order, 'system' - indicates a system-generated order. + enum: ['user', 'system'] + description: The source of the order. 'user' indicates a user-placed order, 'system' indicates a system-generated order. + OrderReason: type: string - enum: - - liquidation - - take_profit_stop_loss - description: >- - The reason for a system-generated order. Present for liquidation and - TP/SL orders. + enum: ['liquidation', 'take_profit_stop_loss'] + description: The reason for a system-generated order. Present for liquidation and TP/SL orders. + LastUpdateReason: type: string - enum: - - '' - - Decrease - - Amend - - MarginCancel - - SelfTradeCancel - - ExpiryCancel - - Trade - - PostOnlyCrossCancel + enum: ['', 'Decrease', 'Amend', 'MarginCancel', 'SelfTradeCancel', 'ExpiryCancel', 'Trade', 'PostOnlyCrossCancel'] + MarginMarketResponse: type: object required: @@ -2190,6 +2061,7 @@ components: properties: market: $ref: '#/components/schemas/MarginMarket' + GetMarginMarketsResponse: type: object required: @@ -2199,6 +2071,7 @@ components: type: array items: $ref: '#/components/schemas/MarginMarket' + GetMarginFillsResponse: type: object required: @@ -2211,6 +2084,7 @@ components: $ref: '#/components/schemas/MarginFill' cursor: type: string + MarginFill: type: object required: @@ -2251,19 +2125,16 @@ components: description: Fill price in fixed-point dollars entry_price: type: string - description: >- - Position entry price used to compute incremental realized PnL for - this fill + description: Position entry price used to compute incremental realized PnL for this fill fees: type: string description: Fees paid on filled contracts, in dollars realized_pnl: type: string - description: >- - Incremental realized PnL contributed by this fill, in fixed-point - dollars + description: Incremental realized PnL contributed by this fill, in fixed-point dollars order_source: $ref: '#/components/schemas/OrderSource' + GetMarginPositionsResponse: type: object required: @@ -2273,6 +2144,7 @@ components: type: array items: $ref: '#/components/schemas/MarginPosition' + MarginPosition: type: object required: @@ -2286,17 +2158,13 @@ components: properties: subaccount: type: integer - description: >- - The subaccount number that holds this position (0 for primary, 1-63 - for subaccounts) + description: The subaccount number that holds this position (0 for primary, 1-63 for subaccounts) market_ticker: type: string description: Market ticker symbol position: $ref: '#/components/schemas/FixedPointCount' - description: >- - Position size as a fixed-point count string (positive = long, - negative = short) + description: Position size as a fixed-point count string (positive = long, negative = short) entry_price: $ref: '#/components/schemas/FixedPointDollars' description: Weighted average entry price of the open position @@ -2306,30 +2174,19 @@ components: margin_used: $ref: '#/components/schemas/FixedPointDollars' nullable: true - description: >- - Maintenance-margin-based capital usage for the open position. Null - when the position shares its asset class with other portfolio-margin - positions in the subaccount, since margin is then computed jointly - for the group and cannot be attributed to a single market. + description: Maintenance-margin-based capital usage for the open position. Null when the position shares its asset class with other portfolio-margin positions in the subaccount, since margin is then computed jointly for the group and cannot be attributed to a single market. fees: $ref: '#/components/schemas/FixedPointDollars' - description: >- - Total fees accumulated over the lifetime of the current open - position, resets when position is fully closed + description: Total fees accumulated over the lifetime of the current open position, resets when position is fully closed roe: type: number format: double nullable: true - description: >- - Return on equity as a percentage (unrealized_pnl / margin_used * - 100). Null when margin_used is zero or not attributable to this - market. + description: Return on equity as a percentage (unrealized_pnl / margin_used * 100). Null when margin_used is zero or not attributable to this market. is_portfolio: type: boolean - description: >- - True when this position is hedged within a portfolio, so margin_used - and roe cannot be attributed to it individually and are not - reported. + description: 'True when this position is hedged within a portfolio, so margin_used and roe cannot be attributed to it individually and are not reported.' + GetMarginTradesResponse: type: object required: @@ -2342,6 +2199,7 @@ components: $ref: '#/components/schemas/MarginTrade' cursor: type: string + MarginTrade: type: object required: @@ -2373,10 +2231,9 @@ components: description: Side of the taker in this trade BookSide: type: string - enum: - - bid - - ask + enum: ['bid', 'ask'] description: The side of an order or trade (bid or ask) + MarginEnabledResponse: type: object required: @@ -2385,6 +2242,7 @@ components: enabled: type: boolean description: Indicates whether margin trading is enabled for the user + NotionalRiskLimitResponse: type: object required: @@ -2393,21 +2251,16 @@ components: properties: default_notional_value_risk_limit: type: string - description: >- - The notional value risk limit for the user as a fixed-point dollar - string with 4 decimal places (e.g., "5000.0000") - example: '5000.0000' + description: The notional value risk limit for the user as a fixed-point dollar string with 4 decimal places (e.g., "5000.0000") + example: "5000.0000" notional_value_risk_limits_by_market_ticker: type: object additionalProperties: type: string - description: >- - Map of market_ticker to notional value risk limit as a fixed-point - dollar string with 4 decimal places (e.g., "5000.0000"). If present, - the market-level risk limit overrides the default notional value - risk limit. + description: Map of market_ticker to notional value risk limit as a fixed-point dollar string with 4 decimal places (e.g., "5000.0000"). If present, the market-level risk limit overrides the default notional value risk limit. example: - market-abc-123: '5000.0000' + "market-abc-123": "5000.0000" + MarginSubaccountBalance: type: object required: @@ -2424,34 +2277,23 @@ components: description: The subaccount number (0 for primary, 1-63 for subaccounts) position_value: $ref: '#/components/schemas/FixedPointDollars' - description: >- - Mark-to-market value of open positions for this subaccount in - fixed-point dollars + description: Mark-to-market value of open positions for this subaccount in fixed-point dollars account_equity: $ref: '#/components/schemas/FixedPointDollars' - description: >- - Account equity for this subaccount in fixed-point dollars. 0 for - self clearing members. + description: Account equity for this subaccount in fixed-point dollars. 0 for self clearing members. maintenance_margin: $ref: '#/components/schemas/FixedPointDollars' - description: >- - Maintenance margin requirement for this subaccount in fixed-point - dollars + description: Maintenance margin requirement for this subaccount in fixed-point dollars initial_margin: $ref: '#/components/schemas/FixedPointDollars' - description: >- - Initial margin requirement for this subaccount in fixed-point - dollars. 0 for self clearing members. + description: Initial margin requirement for this subaccount in fixed-point dollars. 0 for self clearing members. resting_orders_margin: $ref: '#/components/schemas/FixedPointDollars' - description: >- - Margin locked by resting orders for this subaccount in fixed-point - dollars. 0 unless compute_available_balance is passed. + description: Margin locked by resting orders for this subaccount in fixed-point dollars. 0 unless compute_available_balance is passed. available_balance: $ref: '#/components/schemas/FixedPointDollars' - description: >- - Available balance for this subaccount in fixed-point dollars. 0 for - institutional users or if compute_available_balance was not passed. + description: Available balance for this subaccount in fixed-point dollars. 0 for institutional users or if compute_available_balance was not passed. + GetMarginBalanceResponse: type: object required: @@ -2466,6 +2308,7 @@ components: settled_funds: $ref: '#/components/schemas/FixedPointDollars' description: Total settled funds across all subaccounts in fixed-point dollars + MarginRiskPosition: type: object required: @@ -2490,36 +2333,24 @@ components: description: Current mark price for the market in fixed-point dollars position_notional: $ref: '#/components/schemas/FixedPointDollars' - description: >- - Absolute notional value of the position (|qty| * mark_price) in - fixed-point dollars + description: Absolute notional value of the position (|qty| * mark_price) in fixed-point dollars maintenance_margin_required: $ref: '#/components/schemas/FixedPointDollars' - description: >- - Maintenance margin requirement for this position in fixed-point - dollars. Null if margin config is missing. + description: Maintenance margin requirement for this position in fixed-point dollars. Null if margin config is missing. nullable: true position_leverage: type: number format: double - description: >- - Position leverage ratio (position_notional / - maintenance_margin_required). Null when maintenance margin is zero - or config is missing. + description: 'Position leverage ratio (position_notional / maintenance_margin_required). Null when maintenance margin is zero or config is missing.' nullable: true estimated_liquidation_price: $ref: '#/components/schemas/FixedPointDollars' - description: >- - Estimated portfolio-aware liquidation price for this position within - the subaccount. Null when no valid liquidation price exists. + description: 'Estimated portfolio-aware liquidation price for this position within the subaccount. Null when no valid liquidation price exists.' nullable: true is_portfolio: type: boolean - description: >- - True when this position is hedged within a portfolio, so - maintenance_margin_required, position_leverage, and - estimated_liquidation_price cannot be attributed to it individually - and are not reported. + description: 'True when this position is hedged within a portfolio, so maintenance_margin_required, position_leverage, and estimated_liquidation_price cannot be attributed to it individually and are not reported.' + GetMarginRiskResponse: type: object required: @@ -2530,26 +2361,20 @@ components: account_leverage: type: number format: double - description: >- - Account-level leverage (total_position_notional / - total_maintenance_margin). Null when total maintenance margin is - zero. + description: 'Account-level leverage (total_position_notional / total_maintenance_margin). Null when total maintenance margin is zero.' nullable: true total_position_notional: $ref: '#/components/schemas/FixedPointDollars' - description: >- - Sum of absolute position notional values across all positions in - fixed-point dollars + description: Sum of absolute position notional values across all positions in fixed-point dollars total_maintenance_margin: $ref: '#/components/schemas/FixedPointDollars' - description: >- - Sum of maintenance margin requirements across all positions in - fixed-point dollars + description: Sum of maintenance margin requirements across all positions in fixed-point dollars positions: type: array items: $ref: '#/components/schemas/MarginRiskPosition' description: Per-position risk breakdown grouped by subaccount and market + GetMarginFeeTiersResponse: type: object required: @@ -2561,19 +2386,14 @@ components: additionalProperties: type: number format: double - description: >- - A map of margin market ticker to the maker-side fee rate as a - decimal fraction of notional (e.g. 0.0005 = 0.05% = 5 bps). Multiply - notional by this value to compute the fee. + description: A map of margin market ticker to the maker-side fee rate as a decimal fraction of notional (e.g. 0.0005 = 0.05% = 5 bps). Multiply notional by this value to compute the fee. taker_fee_rates: type: object additionalProperties: type: number format: double - description: >- - A map of margin market ticker to the taker-side fee rate as a - decimal fraction of notional (e.g. 0.0012 = 0.12% = 12 bps). - Multiply notional by this value to compute the fee. + description: A map of margin market ticker to the taker-side fee rate as a decimal fraction of notional (e.g. 0.0012 = 0.12% = 12 bps). Multiply notional by this value to compute the fee. + MarginFundingHistoryEntry: type: object required: @@ -2601,9 +2421,7 @@ components: description: Mark price at the time of funding funding_amount: $ref: '#/components/schemas/FixedPointDollars' - description: >- - Dollar amount of the funding payment (positive = received, negative - = paid) + description: Dollar amount of the funding payment (positive = received, negative = paid) quantity: $ref: '#/components/schemas/FixedPointCount' description: Position size at time of funding as a fixed-point count string @@ -2611,6 +2429,7 @@ components: type: integer nullable: true description: Subaccount number (0 for primary) + GetMarginFundingHistoryResponse: type: object required: @@ -2621,6 +2440,7 @@ components: items: $ref: '#/components/schemas/MarginFundingHistoryEntry' description: Array of historical funding payment entries + MarginFundingRate: type: object required: @@ -2643,6 +2463,7 @@ components: mark_price: $ref: '#/components/schemas/FixedPointDollars' description: Mark price at the time of funding + GetMarginHistoricalFundingRatesResponse: type: object required: @@ -2653,6 +2474,7 @@ components: items: $ref: '#/components/schemas/MarginFundingRate' description: Array of historical funding rate entries + GetMarginFundingRateEstimateResponse: type: object required: @@ -2676,6 +2498,7 @@ components: type: string format: date-time description: Timestamp of the next scheduled funding event + GetMarginMarketCandlesticksResponse: type: object required: @@ -2690,6 +2513,7 @@ components: description: Array of candlestick data points for the specified time range. items: $ref: '#/components/schemas/MarginMarketCandlestick' + MarginMarketCandlestick: type: object required: @@ -2708,39 +2532,26 @@ components: description: Unix timestamp for the inclusive end of the candlestick period. bid: $ref: '#/components/schemas/BidAskDistributionHistorical' - description: >- - Open, high, low, close (OHLC) data for buy offers on the market - during the candlestick period. + description: Open, high, low, close (OHLC) data for buy offers on the market during the candlestick period. ask: $ref: '#/components/schemas/BidAskDistributionHistorical' - description: >- - Open, high, low, close (OHLC) data for sell offers on the market - during the candlestick period. + description: Open, high, low, close (OHLC) data for 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 prices on the - market during the candlestick period. + description: Open, high, low, close (OHLC) and more data for trade prices on the market during the candlestick period. volume: $ref: '#/components/schemas/FixedPointCount' - description: >- - Number of contracts traded on the market during the candlestick - period. + description: Number of contracts traded on the market during the candlestick period. volume_notional_value_dollars: $ref: '#/components/schemas/FixedPointDollars' - description: >- - Notional value of contracts traded on the market during the - candlestick period. + description: Notional value of contracts traded on the market during the candlestick period. open_interest: $ref: '#/components/schemas/FixedPointCount' - description: >- - Number of contracts held on the market by end of the candlestick - period (end_period_ts). + description: Number of contracts held on the market by end of the candlestick period (end_period_ts). open_interest_notional_value_dollars: $ref: '#/components/schemas/FixedPointDollars' - description: >- - Notional value of contracts held on the market by end of the - candlestick period (end_period_ts). + description: Notional value of contracts held on the market by end of the candlestick period (end_period_ts). + BidAskDistributionHistorical: type: object required: @@ -2748,10 +2559,7 @@ components: - low - high - close - description: >- - OHLC data for quoted prices on one side of the orderbook during the - candlestick period. These values reflect bid or ask quotes, not executed - trade prices. + description: OHLC data for quoted prices on one side of the orderbook during the candlestick period. These values reflect bid or ask quotes, not executed trade prices. properties: open: $ref: '#/components/schemas/FixedPointDollars' @@ -2765,6 +2573,7 @@ components: close: $ref: '#/components/schemas/FixedPointDollars' description: Quoted price at the end of the candlestick period (in dollars). + PriceDistributionHistorical: type: object required: @@ -2779,52 +2588,38 @@ 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. + parameters: 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 @@ -2839,7 +2634,7 @@ components: maximum: 1000 default: 100 x-oapi-codegen-extra-tags: - validate: omitempty,min=1,max=1000 + validate: "omitempty,min=1,max=1000" MarginOrdersLimitQuery: name: limit in: query @@ -2851,7 +2646,7 @@ components: maximum: 10000 default: 10000 x-oapi-codegen-extra-tags: - validate: omitempty,min=1,max=10000 + validate: "omitempty,min=1,max=10000" MaxTsQuery: name: max_ts in: query @@ -2897,9 +2692,7 @@ components: name: subaccount in: query required: false - 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 minimum: 0 @@ -2915,6 +2708,8 @@ components: tags: - name: account description: Account information endpoints + - name: fcm + description: FCM member specific endpoints - name: exchange description: Exchange status and information endpoints - name: market diff --git a/specs/perps_scm_openapi.yaml b/specs/perps_scm_openapi.yaml index c836cad6..8e665524 100644 --- a/specs/perps_scm_openapi.yaml +++ b/specs/perps_scm_openapi.yaml @@ -155,11 +155,12 @@ paths: settlement estimate. Estimated next crypto settlement amounts for the authenticated - clearing member, including per-subtrader breakdowns. + clearing member, including per-subtrader and per-group breakdowns. - `subtrader_breakdowns` contains at most the 1,000 subtraders with the - largest maintenance margin requirement; `omitted_subtrader_count` - reports how many were left out. `user_breakdown` always aggregates + `subtrader_breakdowns` and `group_breakdowns` together contain at most + the 1,000 entries with the largest maintenance margin requirement; + `omitted_subtrader_count` and `omitted_group_count` report how many + entries of each type were left out. `user_breakdown` always aggregates across all subtraders. responses: @@ -178,11 +179,13 @@ paths: summary: Get Settlement Estimate By Asset Class description: >- Estimated next settlement amounts for the authenticated clearing - member, keyed by asset class, including per-subtrader breakdowns. - Asset classes where the member has no margin activity are omitted. - `subtrader_breakdowns` contains at most the 1,000 subtraders with the - largest maintenance margin requirement; `omitted_subtrader_count` - reports how many were left out. `user_breakdown` always aggregates + member, keyed by asset class, including per-subtrader and per-group + breakdowns. Asset classes where the member has no margin activity are + omitted. + `subtrader_breakdowns` and `group_breakdowns` together contain at most + the 1,000 entries with the largest maintenance margin requirement; + `omitted_subtrader_count` and `omitted_group_count` report how many + entries of each type were left out. `user_breakdown` always aggregates across all subtraders. responses: @@ -288,6 +291,81 @@ paths: '403': { $ref: '#/components/responses/ForbiddenError' } '500': { $ref: '#/components/responses/InternalServerError' } + /fcm/margin/subtrader_groups: + get: + operationId: GetMarginSubtraderGroups + summary: Get Subtrader Groups + description: List the clearing member's margin subtrader groups and their members. + + responses: + '200': + description: Successful response + content: + application/json: + schema: { $ref: '#/components/schemas/GetMarginSubtraderGroupsResponse' } + '400': { $ref: '#/components/responses/BadRequestError' } + '401': { $ref: '#/components/responses/UnauthorizedError' } + '403': { $ref: '#/components/responses/ForbiddenError' } + '500': { $ref: '#/components/responses/InternalServerError' } + post: + operationId: CreateMarginSubtraderGroup + summary: Create Subtrader Group + description: Create a margin subtrader group. Grouped subtraders are margined as one netted portfolio at each settlement. + + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/CreateMarginSubtraderGroupRequest' } + responses: + '200': + description: Group created + content: + application/json: + schema: { $ref: '#/components/schemas/CreateMarginSubtraderGroupResponse' } + '400': { $ref: '#/components/responses/BadRequestError' } + '401': { $ref: '#/components/responses/UnauthorizedError' } + '403': { $ref: '#/components/responses/ForbiddenError' } + '404': { $ref: '#/components/responses/NotFoundError' } + '409': { $ref: '#/components/responses/ConflictError' } + '500': { $ref: '#/components/responses/InternalServerError' } + + /fcm/margin/subtrader_groups/{group_id}: + parameters: + - name: group_id + in: path + required: true + schema: { type: string, format: uuid } + put: + operationId: UpdateMarginSubtraderGroup + summary: Update Subtrader Group + description: Replace a margin subtrader group's membership. + + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/UpdateMarginSubtraderGroupRequest' } + responses: + '200': { description: Group updated } + '400': { $ref: '#/components/responses/BadRequestError' } + '401': { $ref: '#/components/responses/UnauthorizedError' } + '403': { $ref: '#/components/responses/ForbiddenError' } + '404': { $ref: '#/components/responses/NotFoundError' } + '409': { $ref: '#/components/responses/ConflictError' } + '500': { $ref: '#/components/responses/InternalServerError' } + delete: + operationId: DeleteMarginSubtraderGroup + summary: Delete Subtrader Group + description: Delete a margin subtrader group if it exists. + + responses: + '200': { description: Group deleted or already absent } + '400': { $ref: '#/components/responses/BadRequestError' } + '401': { $ref: '#/components/responses/UnauthorizedError' } + '403': { $ref: '#/components/responses/ForbiddenError' } + '500': { $ref: '#/components/responses/InternalServerError' } + components: securitySchemes: kalshiBearer: @@ -313,6 +391,16 @@ components: content: application/json: schema: { $ref: '#/components/schemas/Error' } + NotFoundError: + description: Resource not found + content: + application/json: + schema: { $ref: '#/components/schemas/Error' } + ConflictError: + description: Request conflicts with existing state + content: + application/json: + schema: { $ref: '#/components/schemas/Error' } InternalServerError: description: Internal server error content: @@ -347,6 +435,8 @@ components: - market_price_snapshot - funding_periods - settlement_periods + - maintenance_margin + - maintenance_margin_aggregate url: { type: string, description: Presigned download URL (omitted from logs). } date: { type: string, format: date } created_ts: { type: string, format: date-time } @@ -434,7 +524,8 @@ components: - maintenance_margin_delta_centicents properties: id: { type: string } - subtrader_id: { type: string, description: Empty when not populated. } + subtrader_id: { type: string, description: Empty when not populated or when the subtrader is part of a subtrader group. } + margin_group_id: { type: string, description: Set when the subtrader is part of a subtrader group. } maintenance_margin_centicents: { type: integer, format: int64, description: Maintenance margin requirement after this settlement. } maintenance_margin_delta_centicents: { type: integer, format: int64, description: Change in maintenance margin between the previous settlement and this one. } @@ -539,9 +630,14 @@ components: user_breakdown: { $ref: '#/components/schemas/SettlementEstimate' } subtrader_breakdowns: type: object - description: Map of subtrader ID to that subtrader's settlement estimate. Contains at most the top 1,000 subtraders by maintenance margin requirement. + description: Map of subtrader ID to that portfolio's settlement estimate. + additionalProperties: { $ref: '#/components/schemas/SettlementEstimate' } + group_breakdowns: + type: object + description: Map of margin group ID to that portfolio's netted settlement estimate. additionalProperties: { $ref: '#/components/schemas/SettlementEstimate' } omitted_subtrader_count: { type: integer, format: int64, description: Number of subtraders omitted from subtrader_breakdowns. Their amounts are still included in user_breakdown. } + omitted_group_count: { type: integer, format: int64, description: Number of groups omitted from group_breakdowns. Their amounts are still included in user_breakdown. } prev_settlement_prices: type: object description: Map of market ticker to that market's most recent settlement (mark) price, in centicents. @@ -557,9 +653,14 @@ components: description: Estimated next settlement for this asset class. Null when the member has no activity in this asset class. subtrader_breakdowns: type: object - description: Map of subtrader ID to that subtrader's settlement estimate. Contains at most the top 1,000 subtraders by maintenance margin requirement. + description: Map of subtrader ID to that portfolio's settlement estimate. + additionalProperties: { $ref: '#/components/schemas/SettlementEstimate' } + group_breakdowns: + type: object + description: Map of margin group ID to that portfolio's netted settlement estimate. additionalProperties: { $ref: '#/components/schemas/SettlementEstimate' } omitted_subtrader_count: { type: integer, format: int64, description: Number of subtraders omitted from subtrader_breakdowns. Their amounts are still included in user_breakdown. } + omitted_group_count: { type: integer, format: int64, description: Number of groups omitted from group_breakdowns. Their amounts are still included in user_breakdown. } prev_settlement_prices: type: object description: Map of market ticker to that market's most recent settlement (mark) price, in centicents. @@ -637,3 +738,46 @@ components: cursor: type: string description: Pass as the `cursor` query param to fetch the next page. Absent when there are no more results. + + CreateMarginSubtraderGroupRequest: + type: object + required: [subtrader_ids] + properties: + subtrader_ids: + type: array + minItems: 1 + items: { type: string, minLength: 1 } + description: List of subtrader IDs belonging to the clearing member that will comprise the group. Subtraders must not belong to another group. + + CreateMarginSubtraderGroupResponse: + type: object + required: [group_id] + properties: + group_id: { type: string, format: uuid } + + UpdateMarginSubtraderGroupRequest: + type: object + required: [subtrader_ids] + properties: + subtrader_ids: + type: array + minItems: 1 + items: { type: string, minLength: 1 } + description: Full desired list of subtrader IDs belonging to the clearing member. Subtraders must not belong to another group. + + MarginSubtraderGroup: + type: object + required: [group_id, member_subtrader_ids] + properties: + group_id: { type: string, format: uuid } + member_subtrader_ids: + type: array + items: { type: string } + + GetMarginSubtraderGroupsResponse: + type: object + required: [groups] + properties: + groups: + type: array + items: { $ref: '#/components/schemas/MarginSubtraderGroup' } diff --git a/tests/_contract_support.py b/tests/_contract_support.py index 8dbc955d..90d9b1e0 100644 --- a/tests/_contract_support.py +++ b/tests/_contract_support.py @@ -219,6 +219,11 @@ class Exclusion: http_method="GET", path_template="/live_data/milestone/{milestone_id}", ), + MethodEndpointEntry( + sdk_method="kalshi.resources.live_data.LiveDataResource.get_event", + http_method="GET", + path_template="/live_data/events/{event_ticker}", + ), MethodEndpointEntry( sdk_method="kalshi.resources.live_data.LiveDataResource.get_typed", http_method="GET", @@ -667,12 +672,6 @@ class Exclusion: path_template="/portfolio/subaccounts/transfer", request_body_schema="#/components/schemas/ApplySubaccountTransferRequest", ), - MethodEndpointEntry( - sdk_method="kalshi.resources.subaccounts.SubaccountsResource.transfer_position", - http_method="POST", - path_template="/portfolio/subaccounts/positions/transfer", - request_body_schema="#/components/schemas/ApplySubaccountPositionTransferRequest", - ), MethodEndpointEntry( sdk_method="kalshi.resources.subaccounts.SubaccountsResource.list_balances", http_method="GET", @@ -1524,6 +1523,13 @@ class Exclusion: path_template="/portfolio/margin/subaccounts/transfer", request_body_schema="#/components/schemas/ApplySubaccountTransferRequest", ), + # ── perps FCM ────────────────────────────────────────────────────────── + MethodEndpointEntry( + sdk_method="kalshi.perps.resources.fcm.FcmResource.create_subtrader", + http_method="POST", + path_template="/margin/fcm/subtraders", + request_body_schema="#/components/schemas/CreateMarginFCMSubtraderRequest", + ), ] # SCM/Klear endpoints — validated against ``specs/perps_scm_openapi.yaml``. @@ -1600,6 +1606,29 @@ class Exclusion: http_method="GET", path_template="/margin/settlement_balance_withdrawal", ), + # ── perps SCM subtrader groups ───────────────────────────────────────── + MethodEndpointEntry( + sdk_method="kalshi.perps.klear.resources.margin.MarginResource.list_subtrader_groups", + http_method="GET", + path_template="/fcm/margin/subtrader_groups", + ), + MethodEndpointEntry( + sdk_method="kalshi.perps.klear.resources.margin.MarginResource.create_subtrader_group", + http_method="POST", + path_template="/fcm/margin/subtrader_groups", + request_body_schema="#/components/schemas/CreateMarginSubtraderGroupRequest", + ), + MethodEndpointEntry( + sdk_method="kalshi.perps.klear.resources.margin.MarginResource.update_subtrader_group", + http_method="PUT", + path_template="/fcm/margin/subtrader_groups/{group_id}", + request_body_schema="#/components/schemas/UpdateMarginSubtraderGroupRequest", + ), + MethodEndpointEntry( + sdk_method="kalshi.perps.klear.resources.margin.MarginResource.delete_subtrader_group", + http_method="DELETE", + path_template="/fcm/margin/subtrader_groups/{group_id}", + ), ] # Shared perps exclusion allowlist (same ``(sdk_fqn, field) → Exclusion`` shape diff --git a/tests/integration/test_subaccounts.py b/tests/integration/test_subaccounts.py index aeb5e278..5cdb9349 100644 --- a/tests/integration/test_subaccounts.py +++ b/tests/integration/test_subaccounts.py @@ -46,7 +46,6 @@ "list_balances", "list_transfers", "transfer", - "transfer_position", "update_netting", ], ) @@ -177,48 +176,8 @@ def test_transfer_rejects_invalid_amount( amount_cents=-1, ) - def test_transfer_position_rejects_invalid_price( - self, sync_client: KalshiClient, - ) -> None: - # v3.24.0: `price` is OrderPrice (fixed-point dollars). A negative price - # rejects before any network call, independent of demo position state - # (the old 0-100c cap is gone; the server enforces the upper bound). - with pytest.raises(ValueError): - sync_client.subaccounts.transfer_position( - client_transfer_id=str(uuid.uuid4()), - from_subaccount=0, - to_subaccount=1, - market_ticker="MKT-DOES-NOT-MATTER", - side="yes", - count=1, - price=Decimal("-0.01"), - ) - - def test_transfer_position_smoke( - self, - sync_client: KalshiClient, - ephemeral_subaccount: int, - ) -> None: - # Moving a position requires an open position in the primary subaccount, - # which demo may not have. Exercise the request path and skip cleanly if - # the server refuses (no position, unknown market, etc.). - try: - resp = sync_client.subaccounts.transfer_position( - client_transfer_id=str(uuid.uuid4()), - from_subaccount=0, - to_subaccount=ephemeral_subaccount, - market_ticker="KXBTCD-99DEC31-B1", - side="yes", - count=1, - price=Decimal("0.01"), - ) - except KalshiError as e: - pytest.skip(f"demo refused position transfer (no position to move?): {e}") - assert resp.position_transfer_id -@pytest.mark.integration -@pytest.mark.integration_real_api_only class TestSubaccountsRealApiOnly: """Endpoints demo cannot service. diff --git a/tests/perps/klear/test_margin.py b/tests/perps/klear/test_margin.py index e4b200cc..756e96af 100644 --- a/tests/perps/klear/test_margin.py +++ b/tests/perps/klear/test_margin.py @@ -825,3 +825,84 @@ async def test_async_failed_status( resp = await auth_async_klear_client.margin.settlement_balance_withdrawal(id="wd-9") assert resp.status == "failed" await auth_async_klear_client.close() + + +# --------------------------------------------------------------------------- # +# Subtrader groups +# --------------------------------------------------------------------------- # + + +class TestSubtraderGroups: + @respx.mock + def test_list_subtrader_groups(self, auth_klear_client: KlearClient) -> None: + respx.get(f"{BASE}/fcm/margin/subtrader_groups").mock( + return_value=httpx.Response( + 200, + json={ + "groups": [ + { + "group_id": "11111111-1111-1111-1111-111111111111", + "member_subtrader_ids": ["st-a", "st-b"], + } + ] + }, + ) + ) + resp = auth_klear_client.margin.list_subtrader_groups() + assert len(resp.groups) == 1 + assert resp.groups[0].group_id == "11111111-1111-1111-1111-111111111111" + assert resp.groups[0].member_subtrader_ids == ["st-a", "st-b"] + auth_klear_client.close() + + @respx.mock + def test_create_subtrader_group(self, auth_klear_client: KlearClient) -> None: + route = respx.post(f"{BASE}/fcm/margin/subtrader_groups").mock( + return_value=httpx.Response( + 200, json={"group_id": "22222222-2222-2222-2222-222222222222"} + ) + ) + resp = auth_klear_client.margin.create_subtrader_group( + subtrader_ids=["st-a", "st-b"] + ) + assert resp.group_id == "22222222-2222-2222-2222-222222222222" + assert json.loads(route.calls[0].request.content) == { + "subtrader_ids": ["st-a", "st-b"] + } + # Bearer injected + assert "Authorization" in route.calls[0].request.headers + auth_klear_client.close() + + @respx.mock + def test_update_subtrader_group(self, auth_klear_client: KlearClient) -> None: + gid = "33333333-3333-3333-3333-333333333333" + route = respx.put(f"{BASE}/fcm/margin/subtrader_groups/{gid}").mock( + return_value=httpx.Response(200, json={}) + ) + auth_klear_client.margin.update_subtrader_group( + gid, subtrader_ids=["st-c"] + ) + assert json.loads(route.calls[0].request.content) == { + "subtrader_ids": ["st-c"] + } + auth_klear_client.close() + + @respx.mock + def test_delete_subtrader_group(self, auth_klear_client: KlearClient) -> None: + gid = "44444444-4444-4444-4444-444444444444" + route = respx.delete(f"{BASE}/fcm/margin/subtrader_groups/{gid}").mock( + return_value=httpx.Response(200, json={}) + ) + auth_klear_client.margin.delete_subtrader_group(gid) + assert route.called + auth_klear_client.close() + + def test_create_requires_args(self, auth_klear_client: KlearClient) -> None: + with pytest.raises(TypeError, match="create_subtrader_group"): + auth_klear_client.margin.create_subtrader_group() + auth_klear_client.close() + + def test_create_request_rejects_empty_list(self) -> None: + from kalshi.perps.klear.models.margin import CreateMarginSubtraderGroupRequest + + with pytest.raises(ValidationError): + CreateMarginSubtraderGroupRequest(subtrader_ids=[]) diff --git a/tests/perps/test_fcm.py b/tests/perps/test_fcm.py new file mode 100644 index 00000000..c99c96b9 --- /dev/null +++ b/tests/perps/test_fcm.py @@ -0,0 +1,70 @@ +"""Tests for perps FCM create_subtrader (POST /margin/fcm/subtraders).""" + +from __future__ import annotations + +import json + +import httpx +import pytest +import respx +from pydantic import ValidationError + +from kalshi.errors import AuthRequiredError +from kalshi.perps import PerpsClient, PerpsConfig +from kalshi.perps.models.fcm import ( + CreateMarginFCMSubtraderRequest, + CreateMarginFCMSubtraderResponse, +) + +BASE = "https://external-api.demo.kalshi.co/trade-api/v2" + + +class TestCreateMarginFCMSubtraderRequest: + def test_serializes(self) -> None: + req = CreateMarginFCMSubtraderRequest(subtrader_suffix="desk1") + assert req.model_dump(exclude_none=True, by_alias=True, mode="json") == { + "subtrader_suffix": "desk1" + } + + def test_rejects_bad_suffix(self) -> None: + with pytest.raises(ValidationError): + CreateMarginFCMSubtraderRequest(subtrader_suffix="BAD_SUFFIX") + + def test_forbids_extra(self) -> None: + with pytest.raises(ValidationError): + CreateMarginFCMSubtraderRequest( # type: ignore[call-arg] + subtrader_suffix="desk1", phantom=1 + ) + + +class TestFcmCreateSubtrader: + @respx.mock + def test_create_subtrader_sends_body(self, perps_client: PerpsClient) -> None: + route = respx.post(f"{BASE}/margin/fcm/subtraders").mock( + return_value=httpx.Response(200, json={"subtrader_id": "user_desk1"}) + ) + resp = perps_client.fcm.create_subtrader(subtrader_suffix="desk1") + assert isinstance(resp, CreateMarginFCMSubtraderResponse) + assert resp.subtrader_id == "user_desk1" + assert json.loads(route.calls[0].request.content) == { + "subtrader_suffix": "desk1" + } + + @respx.mock + def test_create_subtrader_with_request_model(self, perps_client: PerpsClient) -> None: + route = respx.post(f"{BASE}/margin/fcm/subtraders").mock( + return_value=httpx.Response(200, json={"subtrader_id": "user_a"}) + ) + req = CreateMarginFCMSubtraderRequest(subtrader_suffix="a") + resp = perps_client.fcm.create_subtrader(request=req) + assert resp.subtrader_id == "user_a" + assert route.called + + def test_requires_args(self, perps_client: PerpsClient) -> None: + with pytest.raises(TypeError, match="create_subtrader"): + perps_client.fcm.create_subtrader() # type: ignore[call-overload] + + def test_unauthenticated_raises(self) -> None: + client = PerpsClient(config=PerpsConfig.demo(max_retries=0)) + with pytest.raises(AuthRequiredError): + client.fcm.create_subtrader(subtrader_suffix="desk1") diff --git a/tests/test_contracts.py b/tests/test_contracts.py index 766b9c06..c8719027 100644 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -1443,9 +1443,6 @@ def _assert_params_match( "#/components/schemas/ApplySubaccountTransferRequest": ( "kalshi.models.subaccounts.ApplySubaccountTransferRequest" ), - "#/components/schemas/ApplySubaccountPositionTransferRequest": ( - "kalshi.models.subaccounts.ApplySubaccountPositionTransferRequest" - ), "#/components/schemas/CreateSubaccountRequest": ( "kalshi.models.subaccounts.CreateSubaccountRequest" ), @@ -1488,6 +1485,10 @@ def _assert_params_match( "#/components/schemas/ApplySubaccountTransferRequest": ( "kalshi.perps.models.transfers.ApplySubaccountTransferRequest" ), + # ── perps FCM ── + "#/components/schemas/CreateMarginFCMSubtraderRequest": ( + "kalshi.perps.models.fcm.CreateMarginFCMSubtraderRequest" + ), } PERPS_SCM_BODY_MODEL_MAP: dict[str, str] = { @@ -1496,6 +1497,13 @@ def _assert_params_match( "#/components/schemas/WithdrawSettlementBalanceRequest": ( "kalshi.perps.klear.models.margin.WithdrawSettlementBalanceRequest" ), + # ── perps SCM subtrader groups ── + "#/components/schemas/CreateMarginSubtraderGroupRequest": ( + "kalshi.perps.klear.models.margin.CreateMarginSubtraderGroupRequest" + ), + "#/components/schemas/UpdateMarginSubtraderGroupRequest": ( + "kalshi.perps.klear.models.margin.UpdateMarginSubtraderGroupRequest" + ), } diff --git a/tests/test_live_data.py b/tests/test_live_data.py index 3e2e6fa2..cf4a86ec 100644 --- a/tests/test_live_data.py +++ b/tests/test_live_data.py @@ -11,6 +11,7 @@ from kalshi.config import KalshiConfig from kalshi.errors import KalshiNotFoundError from kalshi.models.live_data import ( + EventLiveData, GetGameStatsResponse, LiveData, ) @@ -48,6 +49,14 @@ def async_live_data( }, } +_EVENT_LD_JSON = { + "type": "crypto_price", + "details": {"price": "65000.12", "points": [{"t": 1, "v": 1.0}]}, + "is_historical": False, + "default_range": "1h", + "range_options": ["15min", "1h", "1d"], +} + class TestLiveDataModels: def test_live_data_parses(self) -> None: @@ -140,6 +149,38 @@ def test_get_typed_sends_type_path( assert route.called +class TestLiveDataGetEvent: + @respx.mock + def test_get_event_unwraps(self, live_data: LiveDataResource) -> None: + respx.get( + "https://test.kalshi.com/trade-api/v2/live_data/events/KXBTCD-25", + ).mock(return_value=httpx.Response(200, json={"live_data": _EVENT_LD_JSON})) + ld = live_data.get_event("KXBTCD-25") + assert isinstance(ld, EventLiveData) + assert ld.type == "crypto_price" + assert ld.default_range == "1h" + assert ld.range_options == ["15min", "1h", "1d"] + assert ld.is_historical is False + + @respx.mock + def test_get_event_sends_range( + self, live_data: LiveDataResource, + ) -> None: + route = respx.get( + "https://test.kalshi.com/trade-api/v2/live_data/events/KXBTCD-25", + ).mock(return_value=httpx.Response(200, json={"live_data": _EVENT_LD_JSON})) + live_data.get_event("KXBTCD-25", range="15min") + assert dict(route.calls[0].request.url.params) == {"range": "15min"} + + @respx.mock + def test_get_event_404_maps(self, live_data: LiveDataResource) -> None: + respx.get( + "https://test.kalshi.com/trade-api/v2/live_data/events/nope", + ).mock(return_value=httpx.Response(404, json={"message": "not found"})) + with pytest.raises(KalshiNotFoundError): + live_data.get_event("nope") + + class TestLiveDataBatch: @respx.mock def test_batch_explodes_milestone_ids( diff --git a/tests/test_orders.py b/tests/test_orders.py index 29fa8d3c..38fef2bf 100644 --- a/tests/test_orders.py +++ b/tests/test_orders.py @@ -653,6 +653,31 @@ def test_passes_subaccount_query(self, orders: OrdersResource) -> None: body = json.loads(request.content) assert body.get("exchange_index") == 0 + @respx.mock + def test_market_ticker_with_auto_exchange_index( + self, orders: OrdersResource, + ) -> None: + """Spec 3.27.0: market_ticker required when exchange_index is -1.""" + route = respx.post( + "https://test.kalshi.com/trade-api/v2/portfolio/events/orders/ord-1/decrease", + ).mock( + return_value=httpx.Response( + 200, + json={"order_id": "ord-1", "remaining_count": "0", "ts_ms": 0}, + ) + ) + orders.decrease_v2( + "ord-1", + request=DecreaseOrderV2Request( + reduce_by=Decimal("2"), + exchange_index=-1, + market_ticker="MKT-A", + ), + ) + body = json.loads(route.calls[0].request.content) + assert body["exchange_index"] == -1 + assert body["market_ticker"] == "MKT-A" + class TestBatchCreateV2: @respx.mock diff --git a/tests/test_subaccounts.py b/tests/test_subaccounts.py index 6adc1f82..b96fa46e 100644 --- a/tests/test_subaccounts.py +++ b/tests/test_subaccounts.py @@ -21,8 +21,6 @@ KalshiValidationError, ) from kalshi.models.subaccounts import ( - ApplySubaccountPositionTransferRequest, - ApplySubaccountPositionTransferResponse, ApplySubaccountTransferRequest, CreateSubaccountRequest, CreateSubaccountResponse, @@ -284,80 +282,6 @@ def test_create_request_rejects_negative_exchange_index(self) -> None: with pytest.raises(ValidationError): CreateSubaccountRequest(exchange_index=-1) - # ── #464 ApplySubaccountPositionTransferRequest (v3.23.0) ── - def test_position_transfer_request_serializes(self) -> None: - req = ApplySubaccountPositionTransferRequest( - client_transfer_id=_TEST_XFER_ID, - from_subaccount=0, - to_subaccount=1, - market_ticker="MKT-1", - side="yes", - count=5, - price=Decimal("0.50"), - ) - body = req.model_dump(exclude_none=True, by_alias=True, mode="json") - assert body == { - "client_transfer_id": _TEST_XFER_ID, - "from_subaccount": 0, - "to_subaccount": 1, - "market_ticker": "MKT-1", - "side": "yes", - "count": 5, - "price": "0.50", - } - - def test_position_transfer_request_forbids_extra(self) -> None: - with pytest.raises(ValidationError): - ApplySubaccountPositionTransferRequest( # type: ignore[call-arg] - client_transfer_id=_TEST_XFER_ID, - from_subaccount=0, - to_subaccount=1, - market_ticker="MKT-1", - side="yes", - count=5, - price=Decimal("0.50"), - phantom=1, - ) - - def test_position_transfer_request_rejects_bad_side(self) -> None: - with pytest.raises(ValidationError): - ApplySubaccountPositionTransferRequest( - client_transfer_id=_TEST_XFER_ID, - from_subaccount=0, - to_subaccount=1, - market_ticker="MKT-1", - side="maybe", # type: ignore[arg-type] - count=5, - price=Decimal("0.50"), - ) - - def test_position_transfer_request_rejects_zero_count(self) -> None: - with pytest.raises(ValidationError): - ApplySubaccountPositionTransferRequest( - client_transfer_id=_TEST_XFER_ID, - from_subaccount=0, - to_subaccount=1, - market_ticker="MKT-1", - side="yes", - count=0, - price=Decimal("0.50"), - ) - - @pytest.mark.parametrize("bad_price", [Decimal("-0.01"), Decimal("0.123456")]) - def test_position_transfer_request_rejects_bad_price(self, bad_price: Decimal) -> None: - # v3.24.0: `price` is OrderPrice (fixed-point dollars). Negatives and - # sub-$0.0001-tick precision fail at construction; the upper bound is the - # server's to enforce (mirrors CreateOrderRequest — no client-side cap). - with pytest.raises(ValidationError): - ApplySubaccountPositionTransferRequest( - client_transfer_id=_TEST_XFER_ID, - from_subaccount=0, - to_subaccount=1, - market_ticker="MKT-1", - side="yes", - count=5, - price=bad_price, - ) class TestSubaccountsCreate: @respx.mock @@ -397,108 +321,6 @@ def test_create_500_raises(self, subaccounts: SubaccountsResource) -> None: subaccounts.create() -class TestSubaccountsTransferPosition: - @respx.mock - def test_transfer_position_sends_body_and_returns_id( - self, subaccounts: SubaccountsResource, - ) -> None: - route = respx.post( - "https://test.kalshi.com/trade-api/v2/portfolio/subaccounts/positions/transfer", - ).mock(return_value=httpx.Response(200, json={"position_transfer_id": "pt-1"})) - resp = subaccounts.transfer_position( - client_transfer_id=_TEST_XFER_ID, - from_subaccount=0, - to_subaccount=1, - market_ticker="MKT-1", - side="yes", - count=10, - price=Decimal("0.55"), - ) - assert isinstance(resp, ApplySubaccountPositionTransferResponse) - assert resp.position_transfer_id == "pt-1" - assert json.loads(route.calls[0].request.content) == { - "client_transfer_id": _TEST_XFER_ID, - "from_subaccount": 0, - "to_subaccount": 1, - "market_ticker": "MKT-1", - "side": "yes", - "count": 10, - "price": "0.55", - } - - @respx.mock - def test_transfer_position_with_request_model( - self, subaccounts: SubaccountsResource, - ) -> None: - route = respx.post( - "https://test.kalshi.com/trade-api/v2/portfolio/subaccounts/positions/transfer", - ).mock(return_value=httpx.Response(200, json={"position_transfer_id": "pt-2"})) - req = ApplySubaccountPositionTransferRequest( - client_transfer_id=_TEST_XFER_ID, - from_subaccount=1, - to_subaccount=2, - market_ticker="MKT-2", - side="no", - count=3, - price=Decimal("0"), - ) - resp = subaccounts.transfer_position(request=req) - assert resp.position_transfer_id == "pt-2" - assert route.called - - def test_transfer_position_requires_args( - self, subaccounts: SubaccountsResource, - ) -> None: - with pytest.raises(TypeError, match="transfer_position"): - subaccounts.transfer_position(from_subaccount=0) - - def test_transfer_position_rejects_malformed_uuid( - self, subaccounts: SubaccountsResource, - ) -> None: - with pytest.raises(ValueError): - subaccounts.transfer_position( - client_transfer_id="not-a-uuid", - from_subaccount=0, - to_subaccount=1, - market_ticker="MKT-1", - side="yes", - count=1, - price=Decimal("0.01"), - ) - - @respx.mock - def test_transfer_position_400_maps( - self, subaccounts: SubaccountsResource, - ) -> None: - respx.post( - "https://test.kalshi.com/trade-api/v2/portfolio/subaccounts/positions/transfer", - ).mock(return_value=httpx.Response(400, json={"message": "bad"})) - with pytest.raises(KalshiValidationError): - subaccounts.transfer_position( - client_transfer_id=_TEST_XFER_ID, - from_subaccount=0, - to_subaccount=1, - market_ticker="MKT-1", - side="yes", - count=1, - price=Decimal("0.01"), - ) - - def test_transfer_position_unauthenticated_raises_before_http( - self, config: KalshiConfig, - ) -> None: - client = SubaccountsResource(SyncTransport(None, config)) - with pytest.raises(AuthRequiredError): - client.transfer_position( - client_transfer_id=_TEST_XFER_ID, - from_subaccount=0, - to_subaccount=1, - market_ticker="MKT-1", - side="yes", - count=1, - price=Decimal("0.01"), - ) - class TestSubaccountsTransfer: @respx.mock @@ -748,28 +570,6 @@ async def test_transfer( body = json.loads(route.calls[0].request.content) assert body["amount_cents"] == 42 - async def test_transfer_position( - self, - async_subaccounts: AsyncSubaccountsResource, - respx_mock: respx.MockRouter, - ) -> None: - route = respx_mock.post( - "https://test.kalshi.com/trade-api/v2/portfolio/subaccounts/positions/transfer", - ).mock(return_value=httpx.Response(200, json={"position_transfer_id": "pt-async"})) - resp = await async_subaccounts.transfer_position( - client_transfer_id=_TEST_XFER_ID, - from_subaccount=0, - to_subaccount=1, - market_ticker="MKT-1", - side="no", - count=4, - price=Decimal("0.25"), - ) - assert isinstance(resp, ApplySubaccountPositionTransferResponse) - assert resp.position_transfer_id == "pt-async" - body = json.loads(route.calls[0].request.content) - assert body["side"] == "no" - assert body["price"] == "0.25" async def test_list_balances( self, From af87f532faa4261a0e1a0bc5823fdb6327449a0f Mon Sep 17 00:00:00 2001 From: Jeff West Date: Fri, 31 Jul 2026 05:24:45 -0500 Subject: [PATCH 2/2] fix(lint): drop unused Decimal import after transfer_position removal --- tests/integration/test_subaccounts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/test_subaccounts.py b/tests/integration/test_subaccounts.py index 5cdb9349..d6e58c46 100644 --- a/tests/integration/test_subaccounts.py +++ b/tests/integration/test_subaccounts.py @@ -18,7 +18,6 @@ import logging import time import uuid -from decimal import Decimal import pytest