Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,41 @@

All notable changes to kalshi-sdk will be documented in this file.

## 22.10.0 — 2026-11-09

Reconciles nightly spec drift (#574) against OpenAPI 3.36.0 and the latest
perps spec. Additive only. No breaking changes.

### Added

- Optional **`ApiKey.key_type`** (`"rsa"` / `"ed25519"` / `None`). Present
for RSA and Ed25519 keys; omitted when the algorithm is unknown.
- Perps **`SubaccountTypeLiteral`** gains `"managed_isolated"` (a managed
isolated position whose subaccount number is its slot number rather than
the market's isolated subaccount number).
- **`perps.fcm.transfer_position(...)`** (sync and async) —
`POST /margin/fcm/positions/transfers`, 201
`CreateMarginFCMPositionTransferResponse`. FCM members only; Kalshi must
enable the endpoint (400 until then; 409 while transfers are off).
Required: `source_subtrader_id`, `target_subtrader_id`, `side`
(`"long"` / `"short"`), `count`, `price`, `client_transfer_id` (UUID).
Supply `ticker` or `market_id` (ticker wins if both). POST is never
retried. (Closes #574).

### Docs

- Positions / settlement: unsettled includes closed markets awaiting
determination; `count_filter` and `settlement_status` descriptions
clarified.
- Block-trade `accept` may book the caller's side onto a different
subaccount than the proposal named, and a side that has already accepted
may accept again to move until the trade executes.
- Resource-bound MCP OAuth tokens may read owner data with
`read::mcp_portfolio` on settlements and perps orders / fills /
positions / balance / risk.
- Perps `market_lifecycle_v2.is_deactivated` is true when trading is
paused because the market is halted or outside its trading hours.

## 22.9.0 — 2026-11-05

Reconciles nightly spec drift (#571) against OpenAPI 3.36.0 and the latest
Expand Down
38 changes: 32 additions & 6 deletions docs/perps.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ async with AsyncPerpsClient.from_env(demo=True) as perps:
| `margin` | `balance()`, `risk()`, `notional_risk_limit()`, `fee_tiers()`, `fee_tier_rates()`, `api_limits()`, `corporate_actions()` |
| `funding` | `rate_estimate()`, `historical_rates()`, `premium_index()`, `history()` |
| `transfers` | `transfer_instance()`, `create_subaccount()`, `transfer_subaccount()` |
| `fcm` | `create_subtrader(subtrader_suffix=..., require_category_cap=...)`; `risk_controls` / `update_risk_controls` / `delete_risk_controls`; `update_notional_risk_limit` / `delete_notional_risk_limit` |
| `fcm` | `create_subtrader(subtrader_suffix=..., require_category_cap=...)`; `risk_controls` / `update_risk_controls` / `delete_risk_controls`; `update_notional_risk_limit` / `delete_notional_risk_limit`; `transfer_position(...)` |

`markets.bulk_orderbooks(*, tickers)` fetches up to 100 margin orderbooks
in one call (`GET /margin/markets/orderbooks`). `tickers` is required
Expand Down Expand Up @@ -228,10 +228,11 @@ perps.portfolio.cancel_cross_exit_trigger("BTC-PERP", trig.id)
- **Subaccounts** (since the v3.21.0 spec sync) — `MarginPosition` (the
`portfolio.positions()` rows) carries a **required** `subaccount` (`int`)
identifying which subaccount holds the position. `subaccount_type`
(`"cross"` / `"isolated"` / `"auto_rebalance"`, or `None` when the key is
absent) is optional on `MarginPosition`, `MarginSubaccountBalance`,
`MarginRiskPosition`, and `MarginFundingHistoryEntry`. The spec marks it
required; the SDK keeps it optional so older payloads still parse.
(`"cross"` / `"isolated"` / `"auto_rebalance"` / `"managed_isolated"`, or
`None` when the key is absent) is optional on `MarginPosition`,
`MarginSubaccountBalance`, `MarginRiskPosition`, and
`MarginFundingHistoryEntry`. The spec marks it required; the SDK keeps it
optional so older payloads still parse.
- **Portfolio hedging** (since the v3.23.0 spec sync) — `MarginPosition` and
`MarginRiskPosition` carry a **required** `is_portfolio` (`bool`), `True` when
the position is hedged within a portfolio so its margin is computed at the
Expand Down Expand Up @@ -318,7 +319,32 @@ REST-only); subscribe with `session.subscribe(channel="corporate_actions")`.
Upcoming and recent notices are listed by `margin.corporate_actions()`.
`subscribe_market_lifecycle()` yields `MarginMarketLifecycleV2Message` for
activation and deactivation of every margin market (`event_type` is
`activated` or `deactivated`). Ticker filters are not supported on that channel.
`activated` or `deactivated`). `is_deactivated` is true when trading is
paused because the market is halted or outside its trading hours. Ticker
filters are not supported on that channel.

## FCM position transfer

FCM members can move a perps position in one market from a customer
subtrader into a subtrader under the FCM's error account. **Kalshi must
enable this endpoint for your FCM**; until then the server returns 400.
While transfers are switched off for the exchange, requests return 409.
POST is never retried. `client_transfer_id` is a required UUID
idempotency key per source subtrader. Supply `ticker` or `market_id`
(ticker wins if both).

```python
resp = perps.fcm.transfer_position(
source_subtrader_id="user_CUST123",
target_subtrader_id="err_ERR1",
ticker="KXBTCPERP",
side="long",
count="10",
price="63000.00",
client_transfer_id="6d1f0c5a-2b3e-4f7a-9c8d-0e1f2a3b4c5d",
)
print(resp.position_transfer_id)
```

## Self-Clearing-Member "Klear" API

Expand Down
5 changes: 4 additions & 1 deletion docs/resources/api-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@ Auth required throughout (you need an existing key to manage keys).
```python
resp = client.api_keys.list()
for key in resp.api_keys:
print(key.api_key, key.name, key.scopes, key.created_ts)
print(key.api_key_id, key.name, key.scopes, key.key_type)
```

`key_type` is `"rsa"` or `"ed25519"` when the server knows the algorithm,
and `None` when it is omitted (unknown).

## Server-minted pair (`generate`)

The simplest path — Kalshi mints the keypair, you store the private key once:
Expand Down
6 changes: 4 additions & 2 deletions docs/resources/communications.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,10 @@ for proposal in client.communications.block_trade_proposals.list_all(
):
print(proposal.id, proposal.buyer_accepted, proposal.seller_accepted)

# Accept (the counterparty side). The body is optional — pass subtrader_id
# or subaccount only if you trade through a sub-trader / subaccount.
# Accept as the named side. You may book that side onto any of your own
# subaccounts (including one the proposal did not name). Omit both
# subtrader_id and subaccount to keep the proposal's. A side that has
# already accepted may accept again to move until the trade executes.
client.communications.block_trade_proposals.accept(proposal_id)

# Cancel (proposer only). 204, no body.
Expand Down
13 changes: 9 additions & 4 deletions docs/resources/portfolio.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,17 @@ for ep in resp.event_positions:
cursor = resp.cursor
```

`count_filter` filters which fields the response **includes a row for** —
filtering by `"position"` returns only markets where your position is non-zero.
`count_filter` is a comma-separated OR list: `"position"` (non-zero net
contracts, including closed markets awaiting determination) or
`"total_traded"` (any contracts traded, including fully exited positions).
Omit it for no count filter.

`settlement_status` (`SettlementStatusLiteral`: `"unsettled"`, `"settled"`,
`"all"`) selects which live positions to return. Omit it to keep the server
default (`unsettled`). Archived positions stay on `GET /historical/positions`.
`"all"`) filters settlement state, not trading status. Unsettled (the
server default) includes closed markets awaiting determination or
settlement; those rows may still have non-zero quantities and cost basis.
`"settled"` is unarchived settled positions; `"all"` is both live states.
Archived positions stay on `GET /historical/positions`.

## Settlements

Expand Down
2 changes: 1 addition & 1 deletion kalshi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,4 +503,4 @@
"Withdrawal",
]

__version__ = "22.9.0"
__version__ = "22.10.0"
8 changes: 8 additions & 0 deletions kalshi/_contract_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,14 @@ class ContractEntry:
sdk_model="kalshi.perps.models.fcm.UpdateFCMNotionalRiskLimitRequest",
spec_schema="UpdateFCMNotionalRiskLimitRequest",
),
ContractEntry(
sdk_model="kalshi.perps.models.fcm.CreateMarginFCMPositionTransferRequest",
spec_schema="CreateMarginFCMPositionTransferRequest",
),
ContractEntry(
sdk_model="kalshi.perps.models.fcm.CreateMarginFCMPositionTransferResponse",
spec_schema="CreateMarginFCMPositionTransferResponse",
),
ContractEntry(
sdk_model="kalshi.perps.models.portfolio.ExitTrigger",
spec_schema="ExitTrigger",
Expand Down
6 changes: 6 additions & 0 deletions kalshi/models/api_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,16 @@ class ApiKey(BaseModel):
fields do — Kalshi has historically returned JSON null for required
list fields, and silently coercing None -> [] is safer than raising
a Pydantic ValidationError on a field most callers don't use.
``key_type`` is ``rsa`` or ``ed25519`` when the server knows the
algorithm, and omitted (``None``) when unknown.
"""

api_key_id: str
name: str
# Signing algorithm of this key, derived from its stored public key.
# Present for RSA and Ed25519 keys, including existing keys. Omitted
# when the algorithm is unknown.
key_type: ApiKeyTypeLiteral | None = None
scopes: NullableList[str]
# Spec v3.23.0: if set, the key is restricted to this single subaccount
# (0-63). Optional — omitted for account-wide keys and by older servers.
Expand Down
6 changes: 6 additions & 0 deletions kalshi/perps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@
MarginEnabledResponse,
)
from kalshi.perps.models.fcm import (
CreateMarginFCMPositionTransferRequest,
CreateMarginFCMPositionTransferResponse,
FCMAssetClassLiteral,
FCMPositionTransferSideLiteral,
FCMSubtraderNotionalRiskLimit,
FCMSubtraderRiskControls,
GetFCMSubtraderRiskControlsResponse,
Expand Down Expand Up @@ -267,6 +270,8 @@
"CancelMarginOrderResponse",
"ConnectionState",
"CorporateActionPaymentDetail",
"CreateMarginFCMPositionTransferRequest",
"CreateMarginFCMPositionTransferResponse",
"CreateMarginOrderRequest",
"CreateMarginOrderResponse",
"CreateOrderGroupRequest",
Expand All @@ -288,6 +293,7 @@
"ExitTriggerReasonLiteral",
"ExitTriggerStatusLiteral",
"FCMAssetClassLiteral",
"FCMPositionTransferSideLiteral",
"FCMSubtraderNotionalRiskLimit",
"FCMSubtraderRiskControls",
"FeeScheduleLiteral",
Expand Down
6 changes: 6 additions & 0 deletions kalshi/perps/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@
MarginEnabledResponse,
)
from kalshi.perps.models.fcm import (
CreateMarginFCMPositionTransferRequest,
CreateMarginFCMPositionTransferResponse,
FCMAssetClassLiteral,
FCMPositionTransferSideLiteral,
FCMSubtraderNotionalRiskLimit,
FCMSubtraderRiskControls,
GetFCMSubtraderRiskControlsResponse,
Expand Down Expand Up @@ -130,6 +133,8 @@
"BookSide",
"BookSideLiteral",
"CancelMarginOrderResponse",
"CreateMarginFCMPositionTransferRequest",
"CreateMarginFCMPositionTransferResponse",
"CreateMarginOrderRequest",
"CreateMarginOrderResponse",
"CreateOrderGroupRequest",
Expand All @@ -150,6 +155,7 @@
"ExitTriggerReasonLiteral",
"ExitTriggerStatusLiteral",
"FCMAssetClassLiteral",
"FCMPositionTransferSideLiteral",
"FCMSubtraderNotionalRiskLimit",
"FCMSubtraderRiskControls",
"FeeScheduleLiteral",
Expand Down
8 changes: 5 additions & 3 deletions kalshi/perps/models/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,14 @@ class ExchangeInstance(StrEnum):
MARGINED = "margined"


SubaccountTypeLiteral = Literal["cross", "isolated", "auto_rebalance"]
SubaccountTypeLiteral = Literal["cross", "isolated", "auto_rebalance", "managed_isolated"]
"""Spec ``SubaccountType`` — how a subaccount is margined.

``cross`` for the primary account and numbered subaccounts, ``isolated`` for
a market's isolated subaccount, and ``auto_rebalance`` for an auto-rebalanced
isolated position (shares its market's isolated subaccount number).
a market's isolated subaccount, ``auto_rebalance`` for an auto-rebalanced
isolated position (shares its market's isolated subaccount number), and
``managed_isolated`` for a managed isolated position (subaccount number is
its slot number rather than the market's isolated subaccount number).
"""


Expand Down
40 changes: 39 additions & 1 deletion kalshi/perps/models/fcm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
from __future__ import annotations

from typing import Literal
from uuid import UUID

from pydantic import BaseModel, Field

from kalshi.types import DollarDecimal, OrderPrice
from kalshi.types import DollarDecimal, FixedPointCount, OrderPrice

FCMAssetClassLiteral = Literal[
"Crypto",
Expand Down Expand Up @@ -115,3 +116,40 @@ class UpdateFCMNotionalRiskLimitRequest(BaseModel):
notional_value_risk_limit: OrderPrice

model_config = {"extra": "forbid"}


FCMPositionTransferSideLiteral = Literal["long", "short"]
"""Side of a position being moved. Spec inline enum on
``CreateMarginFCMPositionTransferRequest.side``.
"""


class CreateMarginFCMPositionTransferRequest(BaseModel):
"""Body for POST /margin/fcm/positions/transfers.

Moves a perps position in one market from a source FCM subtrader into a
subtrader under the FCM's error account. Supply ``ticker`` or
``market_id`` (ticker wins if both). ``count`` is a FixedPointCount
string of whole contracts; ``price`` is a FixedPointDollars transfer
price per contract (greater than zero). ``client_transfer_id`` is a
required UUID idempotency key per source subtrader.
"""

source_subtrader_id: str
target_subtrader_id: str
side: FCMPositionTransferSideLiteral
count: FixedPointCount = Field(gt=0)
price: OrderPrice = Field(gt=0)
client_transfer_id: UUID
ticker: str | None = None
market_id: str | None = None

model_config = {"extra": "forbid"}


class CreateMarginFCMPositionTransferResponse(BaseModel):
"""Response from POST /margin/fcm/positions/transfers."""

position_transfer_id: str

model_config = {"extra": "allow"}
Loading
Loading