Skip to content

Commit dc2cdcf

Browse files
authored
fix(spec): FCM position transfers, ApiKey.key_type, managed_isolated (#574) — 22.10.0 (#576)
* chore(spec): sync specs for #574 (WIP) * wip(#574): fcm position transfer, key_type, managed_isolated * wip(#574): docs, changelog, version 22.10.0
1 parent 8dfd2f1 commit dc2cdcf

23 files changed

Lines changed: 829 additions & 41 deletions

‎CHANGELOG.md‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,41 @@
22

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

5+
## 22.10.0 — 2026-11-09
6+
7+
Reconciles nightly spec drift (#574) against OpenAPI 3.36.0 and the latest
8+
perps spec. Additive only. No breaking changes.
9+
10+
### Added
11+
12+
- Optional **`ApiKey.key_type`** (`"rsa"` / `"ed25519"` / `None`). Present
13+
for RSA and Ed25519 keys; omitted when the algorithm is unknown.
14+
- Perps **`SubaccountTypeLiteral`** gains `"managed_isolated"` (a managed
15+
isolated position whose subaccount number is its slot number rather than
16+
the market's isolated subaccount number).
17+
- **`perps.fcm.transfer_position(...)`** (sync and async) —
18+
`POST /margin/fcm/positions/transfers`, 201
19+
`CreateMarginFCMPositionTransferResponse`. FCM members only; Kalshi must
20+
enable the endpoint (400 until then; 409 while transfers are off).
21+
Required: `source_subtrader_id`, `target_subtrader_id`, `side`
22+
(`"long"` / `"short"`), `count`, `price`, `client_transfer_id` (UUID).
23+
Supply `ticker` or `market_id` (ticker wins if both). POST is never
24+
retried. (Closes #574).
25+
26+
### Docs
27+
28+
- Positions / settlement: unsettled includes closed markets awaiting
29+
determination; `count_filter` and `settlement_status` descriptions
30+
clarified.
31+
- Block-trade `accept` may book the caller's side onto a different
32+
subaccount than the proposal named, and a side that has already accepted
33+
may accept again to move until the trade executes.
34+
- Resource-bound MCP OAuth tokens may read owner data with
35+
`read::mcp_portfolio` on settlements and perps orders / fills /
36+
positions / balance / risk.
37+
- Perps `market_lifecycle_v2.is_deactivated` is true when trading is
38+
paused because the market is halted or outside its trading hours.
39+
540
## 22.9.0 — 2026-11-05
641

742
Reconciles nightly spec drift (#571) against OpenAPI 3.36.0 and the latest

‎docs/perps.md‎

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ async with AsyncPerpsClient.from_env(demo=True) as perps:
6666
| `margin` | `balance()`, `risk()`, `notional_risk_limit()`, `fee_tiers()`, `fee_tier_rates()`, `api_limits()`, `corporate_actions()` |
6767
| `funding` | `rate_estimate()`, `historical_rates()`, `premium_index()`, `history()` |
6868
| `transfers` | `transfer_instance()`, `create_subaccount()`, `transfer_subaccount()` |
69-
| `fcm` | `create_subtrader(subtrader_suffix=..., require_category_cap=...)`; `risk_controls` / `update_risk_controls` / `delete_risk_controls`; `update_notional_risk_limit` / `delete_notional_risk_limit` |
69+
| `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(...)` |
7070

7171
`markets.bulk_orderbooks(*, tickers)` fetches up to 100 margin orderbooks
7272
in one call (`GET /margin/markets/orderbooks`). `tickers` is required
@@ -228,10 +228,11 @@ perps.portfolio.cancel_cross_exit_trigger("BTC-PERP", trig.id)
228228
- **Subaccounts** (since the v3.21.0 spec sync) — `MarginPosition` (the
229229
`portfolio.positions()` rows) carries a **required** `subaccount` (`int`)
230230
identifying which subaccount holds the position. `subaccount_type`
231-
(`"cross"` / `"isolated"` / `"auto_rebalance"`, or `None` when the key is
232-
absent) is optional on `MarginPosition`, `MarginSubaccountBalance`,
233-
`MarginRiskPosition`, and `MarginFundingHistoryEntry`. The spec marks it
234-
required; the SDK keeps it optional so older payloads still parse.
231+
(`"cross"` / `"isolated"` / `"auto_rebalance"` / `"managed_isolated"`, or
232+
`None` when the key is absent) is optional on `MarginPosition`,
233+
`MarginSubaccountBalance`, `MarginRiskPosition`, and
234+
`MarginFundingHistoryEntry`. The spec marks it required; the SDK keeps it
235+
optional so older payloads still parse.
235236
- **Portfolio hedging** (since the v3.23.0 spec sync) — `MarginPosition` and
236237
`MarginRiskPosition` carry a **required** `is_portfolio` (`bool`), `True` when
237238
the position is hedged within a portfolio so its margin is computed at the
@@ -318,7 +319,32 @@ REST-only); subscribe with `session.subscribe(channel="corporate_actions")`.
318319
Upcoming and recent notices are listed by `margin.corporate_actions()`.
319320
`subscribe_market_lifecycle()` yields `MarginMarketLifecycleV2Message` for
320321
activation and deactivation of every margin market (`event_type` is
321-
`activated` or `deactivated`). Ticker filters are not supported on that channel.
322+
`activated` or `deactivated`). `is_deactivated` is true when trading is
323+
paused because the market is halted or outside its trading hours. Ticker
324+
filters are not supported on that channel.
325+
326+
## FCM position transfer
327+
328+
FCM members can move a perps position in one market from a customer
329+
subtrader into a subtrader under the FCM's error account. **Kalshi must
330+
enable this endpoint for your FCM**; until then the server returns 400.
331+
While transfers are switched off for the exchange, requests return 409.
332+
POST is never retried. `client_transfer_id` is a required UUID
333+
idempotency key per source subtrader. Supply `ticker` or `market_id`
334+
(ticker wins if both).
335+
336+
```python
337+
resp = perps.fcm.transfer_position(
338+
source_subtrader_id="user_CUST123",
339+
target_subtrader_id="err_ERR1",
340+
ticker="KXBTCPERP",
341+
side="long",
342+
count="10",
343+
price="63000.00",
344+
client_transfer_id="6d1f0c5a-2b3e-4f7a-9c8d-0e1f2a3b4c5d",
345+
)
346+
print(resp.position_transfer_id)
347+
```
322348

323349
## Self-Clearing-Member "Klear" API
324350

‎docs/resources/api-keys.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,12 @@ Auth required throughout (you need an existing key to manage keys).
3434
```python
3535
resp = client.api_keys.list()
3636
for key in resp.api_keys:
37-
print(key.api_key, key.name, key.scopes, key.created_ts)
37+
print(key.api_key_id, key.name, key.scopes, key.key_type)
3838
```
3939

40+
`key_type` is `"rsa"` or `"ed25519"` when the server knows the algorithm,
41+
and `None` when it is omitted (unknown).
42+
4043
## Server-minted pair (`generate`)
4144

4245
The simplest path — Kalshi mints the keypair, you store the private key once:

‎docs/resources/communications.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,10 @@ for proposal in client.communications.block_trade_proposals.list_all(
210210
):
211211
print(proposal.id, proposal.buyer_accepted, proposal.seller_accepted)
212212

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

217219
# Cancel (proposer only). 204, no body.

‎docs/resources/portfolio.md‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,17 @@ for ep in resp.event_positions:
111111
cursor = resp.cursor
112112
```
113113

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

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

121126
## Settlements
122127

‎kalshi/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,4 +503,4 @@
503503
"Withdrawal",
504504
]
505505

506-
__version__ = "22.9.0"
506+
__version__ = "22.10.0"

‎kalshi/_contract_map.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,14 @@ class ContractEntry:
10461046
sdk_model="kalshi.perps.models.fcm.UpdateFCMNotionalRiskLimitRequest",
10471047
spec_schema="UpdateFCMNotionalRiskLimitRequest",
10481048
),
1049+
ContractEntry(
1050+
sdk_model="kalshi.perps.models.fcm.CreateMarginFCMPositionTransferRequest",
1051+
spec_schema="CreateMarginFCMPositionTransferRequest",
1052+
),
1053+
ContractEntry(
1054+
sdk_model="kalshi.perps.models.fcm.CreateMarginFCMPositionTransferResponse",
1055+
spec_schema="CreateMarginFCMPositionTransferResponse",
1056+
),
10491057
ContractEntry(
10501058
sdk_model="kalshi.perps.models.portfolio.ExitTrigger",
10511059
spec_schema="ExitTrigger",

‎kalshi/models/api_keys.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,16 @@ class ApiKey(BaseModel):
3131
fields do — Kalshi has historically returned JSON null for required
3232
list fields, and silently coercing None -> [] is safer than raising
3333
a Pydantic ValidationError on a field most callers don't use.
34+
``key_type`` is ``rsa`` or ``ed25519`` when the server knows the
35+
algorithm, and omitted (``None``) when unknown.
3436
"""
3537

3638
api_key_id: str
3739
name: str
40+
# Signing algorithm of this key, derived from its stored public key.
41+
# Present for RSA and Ed25519 keys, including existing keys. Omitted
42+
# when the algorithm is unknown.
43+
key_type: ApiKeyTypeLiteral | None = None
3844
scopes: NullableList[str]
3945
# Spec v3.23.0: if set, the key is restricted to this single subaccount
4046
# (0-63). Optional — omitted for account-wide keys and by older servers.

‎kalshi/perps/__init__.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@
8282
MarginEnabledResponse,
8383
)
8484
from kalshi.perps.models.fcm import (
85+
CreateMarginFCMPositionTransferRequest,
86+
CreateMarginFCMPositionTransferResponse,
8587
FCMAssetClassLiteral,
88+
FCMPositionTransferSideLiteral,
8689
FCMSubtraderNotionalRiskLimit,
8790
FCMSubtraderRiskControls,
8891
GetFCMSubtraderRiskControlsResponse,
@@ -267,6 +270,8 @@
267270
"CancelMarginOrderResponse",
268271
"ConnectionState",
269272
"CorporateActionPaymentDetail",
273+
"CreateMarginFCMPositionTransferRequest",
274+
"CreateMarginFCMPositionTransferResponse",
270275
"CreateMarginOrderRequest",
271276
"CreateMarginOrderResponse",
272277
"CreateOrderGroupRequest",
@@ -288,6 +293,7 @@
288293
"ExitTriggerReasonLiteral",
289294
"ExitTriggerStatusLiteral",
290295
"FCMAssetClassLiteral",
296+
"FCMPositionTransferSideLiteral",
291297
"FCMSubtraderNotionalRiskLimit",
292298
"FCMSubtraderRiskControls",
293299
"FeeScheduleLiteral",

‎kalshi/perps/models/__init__.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@
2828
MarginEnabledResponse,
2929
)
3030
from kalshi.perps.models.fcm import (
31+
CreateMarginFCMPositionTransferRequest,
32+
CreateMarginFCMPositionTransferResponse,
3133
FCMAssetClassLiteral,
34+
FCMPositionTransferSideLiteral,
3235
FCMSubtraderNotionalRiskLimit,
3336
FCMSubtraderRiskControls,
3437
GetFCMSubtraderRiskControlsResponse,
@@ -130,6 +133,8 @@
130133
"BookSide",
131134
"BookSideLiteral",
132135
"CancelMarginOrderResponse",
136+
"CreateMarginFCMPositionTransferRequest",
137+
"CreateMarginFCMPositionTransferResponse",
133138
"CreateMarginOrderRequest",
134139
"CreateMarginOrderResponse",
135140
"CreateOrderGroupRequest",
@@ -150,6 +155,7 @@
150155
"ExitTriggerReasonLiteral",
151156
"ExitTriggerStatusLiteral",
152157
"FCMAssetClassLiteral",
158+
"FCMPositionTransferSideLiteral",
153159
"FCMSubtraderNotionalRiskLimit",
154160
"FCMSubtraderRiskControls",
155161
"FeeScheduleLiteral",

0 commit comments

Comments
 (0)