Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cf45c1d
perps: client foundation — PerpsClient/AsyncPerpsClient, PerpsConfig,…
TexasCoding Jun 4, 2026
456ae85
perps: exchange status, enabled flag & risk parameters resource (#389)
TexasCoding Jun 4, 2026
dc9f3a7
perps: markets resource — list/get, orderbook, candlesticks (#390)
TexasCoding Jun 5, 2026
b742cf5
perps: orders resource — create/get/list/cancel/decrease/amend + FCM …
TexasCoding Jun 5, 2026
f2a4a92
perps: order groups resource — create/get/list/delete/reset/trigger/u…
TexasCoding Jun 5, 2026
2e0350e
perps: portfolio reads — positions, fills, trades (#393)
TexasCoding Jun 5, 2026
dd52a51
perps: balance, risk, notional risk limit & fee tiers resource (#394)
TexasCoding Jun 5, 2026
8c309c5
perps: funding resource — rate estimate, historical rates, payment hi…
TexasCoding Jun 5, 2026
52a07d4
perps: transfers & subaccounts resource — intra-exchange + margin sub…
TexasCoding Jun 5, 2026
b08fdfc
perps: wire REST resource exports + contract-drift maps (#390-396)
TexasCoding Jun 5, 2026
9ed7c9f
perps: WebSocket foundation + channel models (#397, #398)
TexasCoding Jun 5, 2026
fbc4c6d
perps: SCM/Klear cookie-session auth + 9 settlement endpoints (#399, …
TexasCoding Jun 5, 2026
6fa0583
perps: docs, examples, CHANGELOG, version bump & demo integration tes…
TexasCoding Jun 5, 2026
49cc718
perps: address PR #403 code-review findings
TexasCoding Jun 5, 2026
85281ad
Merge branch 'main' into feat/perps-api
TexasCoding Jun 5, 2026
f8cc960
perps(ws): raise on error acks, persist sub updates, fix recv-loop de…
TexasCoding Jun 5, 2026
da953b0
perps(klear): reset session on re-login + redact secrets in validatio…
TexasCoding Jun 5, 2026
f2ef973
perps: cursor optional, order-size guards, shard exclusivity, partial…
TexasCoding Jun 5, 2026
4bdc13a
perps(ws): address PR #403 bot review — model-built orphan-unsubscrib…
TexasCoding Jun 5, 2026
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
83 changes: 55 additions & 28 deletions .github/workflows/spec-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,20 @@ jobs:
id: snapshot
run: |
set -euo pipefail
had_old_openapi=false
had_old_asyncapi=false
if [ -f specs/openapi.yaml ]; then
cp specs/openapi.yaml /tmp/openapi.yaml.old
had_old_openapi=true
fi
if [ -f specs/asyncapi.yaml ]; then
cp specs/asyncapi.yaml /tmp/asyncapi.yaml.old
had_old_asyncapi=true
fi
echo "had_old_openapi=${had_old_openapi}" >> "$GITHUB_OUTPUT"
echo "had_old_asyncapi=${had_old_asyncapi}" >> "$GITHUB_OUTPUT"
snapshot_one() {
# $1 = spec filename, $2 = GITHUB_OUTPUT key
if [ -f "specs/$1" ]; then
cp "specs/$1" "/tmp/$1.old"
echo "$2=true" >> "$GITHUB_OUTPUT"
else
echo "$2=false" >> "$GITHUB_OUTPUT"
fi
}
snapshot_one openapi.yaml had_old_openapi
snapshot_one asyncapi.yaml had_old_asyncapi
snapshot_one perps_openapi.yaml had_old_perps_openapi
snapshot_one perps_asyncapi.yaml had_old_perps_asyncapi
snapshot_one perps_scm_openapi.yaml had_old_perps_scm_openapi

- name: Download latest specs
run: uv run python scripts/sync_spec.py
Expand All @@ -72,24 +74,34 @@ jobs:
id: diff
run: |
set -euo pipefail
openapi_changed=true
asyncapi_changed=true
if [ "${{ steps.snapshot.outputs.had_old_openapi }}" = "true" ] \
&& cmp -s /tmp/openapi.yaml.old specs/openapi.yaml; then
openapi_changed=false
fi
if [ "${{ steps.snapshot.outputs.had_old_asyncapi }}" = "true" ] \
&& cmp -s /tmp/asyncapi.yaml.old specs/asyncapi.yaml; then
asyncapi_changed=false
fi
if $openapi_changed || $asyncapi_changed; then
# changed_one: echoes "false" when a prior snapshot exists and is
# byte-identical to the freshly downloaded spec; "true" otherwise
# (changed, or first-ever fetch).
changed_one() {
# $1 = spec filename, $2 = had_old flag
if [ "$2" = "true" ] && cmp -s "/tmp/$1.old" "specs/$1"; then
echo false
else
echo true
fi
}
openapi_changed=$(changed_one openapi.yaml "${{ steps.snapshot.outputs.had_old_openapi }}")
asyncapi_changed=$(changed_one asyncapi.yaml "${{ steps.snapshot.outputs.had_old_asyncapi }}")
perps_openapi_changed=$(changed_one perps_openapi.yaml "${{ steps.snapshot.outputs.had_old_perps_openapi }}")
perps_asyncapi_changed=$(changed_one perps_asyncapi.yaml "${{ steps.snapshot.outputs.had_old_perps_asyncapi }}")
perps_scm_changed=$(changed_one perps_scm_openapi.yaml "${{ steps.snapshot.outputs.had_old_perps_scm_openapi }}")
if $openapi_changed || $asyncapi_changed || $perps_openapi_changed \
|| $perps_asyncapi_changed || $perps_scm_changed; then
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "openapi_changed=${openapi_changed}" >> "$GITHUB_OUTPUT"
echo "asyncapi_changed=${asyncapi_changed}" >> "$GITHUB_OUTPUT"
echo "Spec changes detected — openapi=${openapi_changed}, asyncapi=${asyncapi_changed}"
echo "perps_openapi_changed=${perps_openapi_changed}" >> "$GITHUB_OUTPUT"
echo "perps_asyncapi_changed=${perps_asyncapi_changed}" >> "$GITHUB_OUTPUT"
echo "perps_scm_changed=${perps_scm_changed}" >> "$GITHUB_OUTPUT"
echo "Spec changes detected — openapi=${openapi_changed}, asyncapi=${asyncapi_changed}, perps_openapi=${perps_openapi_changed}, perps_asyncapi=${perps_asyncapi_changed}, perps_scm=${perps_scm_changed}"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "Both specs unchanged — nothing to do."
echo "All specs unchanged — nothing to do."
fi

- name: Extract spec metadata for report
Expand Down Expand Up @@ -154,8 +166,14 @@ jobs:
set -euo pipefail
openapi_sha=$(sha256sum specs/openapi.yaml | awk '{print $1}')
asyncapi_sha=$(sha256sum specs/asyncapi.yaml | awk '{print $1}')
perps_openapi_sha=$(sha256sum specs/perps_openapi.yaml | awk '{print $1}')
perps_asyncapi_sha=$(sha256sum specs/perps_asyncapi.yaml | awk '{print $1}')
perps_scm_sha=$(sha256sum specs/perps_scm_openapi.yaml | awk '{print $1}')
echo "openapi_sha=${openapi_sha}" >> "$GITHUB_OUTPUT"
echo "asyncapi_sha=${asyncapi_sha}" >> "$GITHUB_OUTPUT"
echo "perps_openapi_sha=${perps_openapi_sha}" >> "$GITHUB_OUTPUT"
echo "perps_asyncapi_sha=${perps_asyncapi_sha}" >> "$GITHUB_OUTPUT"
echo "perps_scm_sha=${perps_scm_sha}" >> "$GITHUB_OUTPUT"

- name: Ensure spec-drift label exists
if: steps.diff.outputs.changed == 'true'
Expand Down Expand Up @@ -189,13 +207,22 @@ jobs:
REMOVED_CHANNELS: ${{ steps.meta.outputs.removed_channels }}
OPENAPI_SHA: ${{ steps.checksums.outputs.openapi_sha }}
ASYNCAPI_SHA: ${{ steps.checksums.outputs.asyncapi_sha }}
PERPS_OPENAPI_CHANGED: ${{ steps.diff.outputs.perps_openapi_changed }}
PERPS_ASYNCAPI_CHANGED: ${{ steps.diff.outputs.perps_asyncapi_changed }}
PERPS_SCM_CHANGED: ${{ steps.diff.outputs.perps_scm_changed }}
PERPS_OPENAPI_SHA: ${{ steps.checksums.outputs.perps_openapi_sha }}
PERPS_ASYNCAPI_SHA: ${{ steps.checksums.outputs.perps_asyncapi_sha }}
PERPS_SCM_SHA: ${{ steps.checksums.outputs.perps_scm_sha }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail

# Drift fingerprint: sha256(openapi_sha || asyncapi_sha). Stable across
# re-runs against the same upstream; changes the moment either spec does.
FINGERPRINT=$(printf '%s%s' "${OPENAPI_SHA}" "${ASYNCAPI_SHA}" | sha256sum | awk '{print $1}')
# Drift fingerprint: sha256 over every spec's sha. Stable across re-runs
# against the same upstream; changes the moment ANY spec does (so a
# perps-only change still opens a fresh drift issue).
FINGERPRINT=$(printf '%s%s%s%s%s' \
"${OPENAPI_SHA}" "${ASYNCAPI_SHA}" "${PERPS_OPENAPI_SHA}" \
"${PERPS_ASYNCAPI_SHA}" "${PERPS_SCM_SHA}" | sha256sum | awk '{print $1}')
export FINGERPRINT

# Dedup: skip if an open spec-drift issue already embeds this fingerprint.
Expand Down
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,59 @@

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

## 3.2.0 — 2026-06-05

Adds full SDK support for the Kalshi **Perps (margin) API** — a separate
perpetual-futures exchange — as standalone clients alongside the existing
prediction-API surface. Additive release: no changes to `KalshiClient`.

### Added

- **`PerpsClient` / `AsyncPerpsClient`** — standalone clients for the perps
exchange (`external-api.kalshi.com` / demo `external-api.demo.kalshi.co`,
`/trade-api/v2`), with their own `PerpsConfig` and a separate `KALSHI_PERPS_*`
credential namespace. They reuse the prediction-API RSA-PSS signer and HTTP
transport unchanged. Resource families: `exchange` (status / enabled gate /
risk parameters), `markets` (list / get / orderbook / candlesticks), `orders`
(create / get / list / cancel / decrease / amend + FCM), `order_groups`,
`portfolio` (positions / fills / trades), `margin` (balance / risk /
notional risk limit / fee tiers), `funding` (rate estimate / historical /
history), and `transfers` (intra-exchange-instance + margin subaccounts).
Margin order side is `bid` / `ask`; prices are `DollarDecimal`
(FixedPointDollars), counts `FixedPointCount`.
- **`PerpsWebSocket`** — the perps margin WebSocket
(`external-api-margin-ws.kalshi.com`, `/trade-api/ws/v2/margin`) with six typed
channels (`subscribe_orderbook_delta`, `subscribe_ticker` — carrying
`funding_rate` + `next_funding_time_ms`, `subscribe_trade`, `subscribe_fill`,
`subscribe_user_orders`, `subscribe_order_group`). Reuses the event-contract
WS connection / sequence-gap / backpressure machinery; perps WS timestamps are
Unix epoch **milliseconds** (`*_ms` fields).
- **`KlearClient` / `AsyncKlearClient`** — the Self-Clearing-Member "Klear"
settlement API (`api.klear.kalshi.com` / demo `demo-api.kalshi.co`,
`/klear-api/v1`) with a third auth model: **cookie-session + MFA** via
`login(email=..., password=..., code=...)`. Resource `margin` covers reports,
active/historical obligations, settlement estimate, settlement + guaranty-fund
balances, settlement-balance history, and settlement-balance withdrawal.
Klear money fields are integer centicents; the single real-money write
(`withdraw_settlement_balance`) validates a positive amount at construction.
Credentials and the session cookie are never logged or shown in `repr()`.
- `docs/perps.md` (+ mkdocs nav), README "Perps (margin) trading" section, and
runnable `examples/perps_create_order.py` / `perps_stream_ticker.py` /
`perps_balance_risk.py`.

### Internal

- Vendored the three perps specs (`specs/perps_openapi.yaml`,
`perps_asyncapi.yaml`, `perps_scm_openapi.yaml`); `scripts/sync_spec.py` and the
weekly spec-sync workflow now fetch/diff/checksum them and fold their sha256
into the drift fingerprint (preserving the `contents: read` + `issues: write`
security model).
- Parameterized the contract-drift harness per spec: `TestPerps*Drift` /
`TestPerpsScm*Drift` validate the perps REST + SCM surfaces against their own
specs, alongside the existing prediction-API drift suites.
- README / `docs/index.md` banners note the perps surface (34 REST operations,
6 WS channels, 10 SCM operations).

## 3.1.0 — 2026-06-03

OpenAPI + AsyncAPI spec sync from v3.19.0 → v3.20.0 (`#385`). Adds the
Expand Down
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ A professional, spec-first Python SDK for the [Kalshi](https://kalshi.com) predi
[![Type checked: mypy strict](https://img.shields.io/badge/mypy-strict-blue.svg)](https://mypy.readthedocs.io/)

- **Full coverage** of the Kalshi REST API (99 operations across 19 resources, OpenAPI v3.20.0) and WebSocket API (11 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 (10 operations). See [Perps (margin) trading](#perps-margin-trading).
- **V2 event-market orders**: `create_v2` / `amend_v2` / `decrease_v2` / `cancel_v2` plus batched variants on `/portfolio/events/orders/*`. Legacy `/portfolio/orders` keeps working — deprecated no earlier than May 6, 2026.
- **Funding & cost introspection**: `portfolio.deposits()`, `portfolio.withdrawals()`, `account.endpoint_costs()`.
- **Sync and async** clients sharing one transport — no thread-pool wrapping.
Expand Down Expand Up @@ -202,6 +203,53 @@ through the generic `subscribe(channel, ...)` escape hatch. See
[docs/websockets.md](docs/websockets.md#the-11-channels) for the full
channel table.

## Perps (margin) trading

Kalshi's **Perps** (perpetual futures / margin) API is a separate exchange on its
own host. The SDK exposes it through standalone `PerpsClient` / `AsyncPerpsClient`
(and `PerpsWebSocket`), reusing the same RSA-PSS auth as `KalshiClient` but with
**separate API keys** issued for the perps exchange — prod base URL
`https://external-api.kalshi.com/trade-api/v2`, demo
`https://external-api.demo.kalshi.co/trade-api/v2`.

```python
from kalshi import PerpsClient

# Reads KALSHI_PERPS_KEY_ID + KALSHI_PERPS_PRIVATE_KEY[_PATH] (separate from KALSHI_*).
with PerpsClient.from_env(demo=True) as perps:
print(perps.exchange.status()) # is the margin exchange trading?
for m in perps.markets.list(status="active"):
print(m.ticker, m.bid, m.ask)
print(perps.margin.balance()) # per-subaccount balance breakdown
```

```python
import asyncio
from kalshi import AsyncPerpsClient

async def main() -> None:
async with AsyncPerpsClient.from_env(demo=True) as perps:
async for order in perps.orders.list_all():
print(order.order_id, order.price, order.remaining_count)

asyncio.run(main())
```

Resource families on the perps client: `exchange`, `markets`, `orders`,
`order_groups`, `portfolio`, `margin` (balance/risk/fees), `funding`, `transfers`.
Real-time streaming via `PerpsWebSocket` — `subscribe_ticker` (carries
`funding_rate` + `next_funding_time_ms`), `subscribe_orderbook_delta`,
`subscribe_trade`, `subscribe_fill`, `subscribe_user_orders`,
`subscribe_order_group`.

Prices are `DollarDecimal` (FixedPointDollars, up to 6 decimals); counts are
`FixedPointCount` (2 decimals, `_fp` wire suffix). REST timestamps are RFC3339;
**WebSocket timestamps are Unix epoch milliseconds** (`*_ms` fields). The
Self-Clearing-Member "Klear" settlement API (margin reports, settlement balances,
obligations, withdrawals) is a third surface exposed via `KlearClient`, which uses
**cookie-session + MFA** login (`client.login(email=..., password=..., code=...)`)
rather than RSA-PSS. Full guide: [docs/perps.md](docs/perps.md).

## Error handling

All SDK errors inherit from `KalshiError`:
Expand Down
4 changes: 4 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ markets API.
reconnection (with resubscribe-window frame stashing for high-volume channels),
backpressure strategies, and an in-memory orderbook builder. Async-only —
access via `AsyncKalshiClient.ws`.
- **Perps (margin) API** — standalone `PerpsClient` / `AsyncPerpsClient` +
`PerpsWebSocket` for the perpetual-futures exchange (34 REST operations, 6 WS
channels), and a `KlearClient` for the Self-Clearing-Member settlement API
(10 operations, cookie-session + MFA auth). See [Perps](perps.md).
- **Sync and async parity** — `KalshiClient` and `AsyncKalshiClient` share one
transport implementation; method names, kwargs, return types, and error behavior
are identical. The one exception is WebSocket access, which is async-only.
Expand Down
Loading