Skip to content

Latest commit

 

History

History
436 lines (371 loc) · 21.4 KB

perps.md

File metadata and controls

436 lines (371 loc) · 21.4 KB

Perps (margin) trading

Kalshi's Perps (perpetual futures / margin) API is a separate exchange on its own host. The SDK exposes it through standalone clients rather than a namespace on KalshiClient, because the perps exchange uses a different host and Kalshi recommends separate API keys for it.

Surface Client Host (prod / demo) Auth
Perps REST PerpsClient / AsyncPerpsClient external-api.kalshi.com / external-api.demo.kalshi.co (/trade-api/v2) RSA-PSS (same signer as KalshiClient)
Perps WebSocket PerpsWebSocket external-api-margin-ws.kalshi.com / external-api-margin-ws.demo.kalshi.co (/trade-api/ws/v2/margin) RSA-PSS apiKey
SCM "Klear" KlearClient / AsyncKlearClient api.klear.kalshi.com / demo-api.kalshi.co (/klear-api/v1) Bearer admin_user_id:access_token

The RSA-PSS signer and the HTTP transport are reused unchanged from the prediction API — only the host, config, and credentials differ.

Client construction

from kalshi import PerpsClient

# Explicit credentials:
with PerpsClient(
    key_id="your-perps-key-id",
    private_key_path="~/.kalshi/perps_key.pem",
    demo=True,
) as perps:
    print(perps.exchange.status())

PerpsConfig.production() / PerpsConfig.demo() build the canonical environments; pass a PerpsConfig for full control (timeouts, retry policy, custom JSON loaders). See Authentication for how RSA-PSS keys are generated and Configuration for the config surface (shared with KalshiConfig).

Environment variables (from_env)

Perps credentials live under a separate KALSHI_PERPS_* namespace so they never collide with the prediction-API KALSHI_* vars:

Variable Purpose
KALSHI_PERPS_KEY_ID perps API key id (omit for an unauthenticated client)
KALSHI_PERPS_PRIVATE_KEY / KALSHI_PERPS_PRIVATE_KEY_PATH RSA private key (PEM string or file)
KALSHI_PERPS_PRIVATE_KEY_PASSPHRASE optional passphrase for an encrypted PEM (a password= kwarg overrides it)
KALSHI_PERPS_API_BASE_URL optional REST base-URL override
KALSHI_PERPS_WS_BASE_URL optional WS base-URL override (keeps the WS feed off production when overriding REST)
KALSHI_PERPS_DEMO "true" routes to the demo exchange
from kalshi import AsyncPerpsClient

async with AsyncPerpsClient.from_env(demo=True) as perps:
    ...

Resource families

Attribute Endpoints
exchange status(), enabled() (per-member access gate), risk_parameters()
markets list(), get(), orderbook(), bulk_orderbooks(*, tickers), candlesticks(), schedules(*, tickers)
orders create(), get(), list() / list_all(), cancel(), cancel_all(), decrease(), amend()
order_groups list(), get(), create(), delete(), reset(), trigger(), update_limit()
portfolio positions(), fills() / fills_all(), trades() / trades_all(), cross/isolated exit triggers
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

markets.bulk_orderbooks(*, tickers) fetches up to 100 margin orderbooks in one call (GET /margin/markets/orderbooks). tickers is required (1–100) and serializes as a comma-separated query string. A ticker that is not a margin market returns 404.

books = perps.markets.bulk_orderbooks(tickers=["BTC-PERP", "ETH-PERP"])
for ob in books:
    print(ob.ticker, len(ob.bids), len(ob.asks))

schedules = perps.markets.schedules(tickers=["BTC-PERP", "ETH-PERP"])
for ticker, details in schedules.items():
    print(ticker, len(details.kalshi_trading_holidays))

markets.schedules(*, tickers) returns Kalshi trading-holiday closures (GET /margin/markets/schedules) as a ticker → MarginMarketScheduleDetails map. tickers is required (1–100, each 1–200 characters) and serializes as a comma-separated query string. Regular daily and weekend closures are not included. Each holiday window's spec field from is start on MarginHolidayWindow (until is the exclusive end).

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. orders.amend(..., expiration_time=) is int64 Unix seconds: omit it to keep the current expiry, or pass 0 to clear it (good-till-canceled). orders.create(..., market_version=) is the expected market version (starts at 1 on MarginMarket.market_version). If set and the market's current version differs, the server rejects with HTTP 409 / market_version_mismatch. Omit to skip the check. orders.create(..., market_id=) / orders.amend(..., market_id=) is a stable exchange market UUID. It may be supplied with ticker (ticker takes precedence when both are set). Omit so the key is not sent. orders.create(..., subaccount=) is optional (0 = primary). Omit so the key is not sent. portfolio.fills() / portfolio.positions() omit subaccount to return rows across all subaccounts (subaccount-restricted API keys still only see their locked subaccount). Pass subaccount=0 for the primary. portfolio.trades() / trades_all() take an optional ticker; omit it to retrieve public trades across all margin markets.

!!! warning "Deprecated in v7.2.0 — list_fcm / list_all_fcm" Kalshi removed GET /margin/fcm/orders from the perps OpenAPI. The SDK methods still exist (sync + async) but emit a DeprecationWarning and may 404 against the live API. They will be removed in a future major release once the removal is confirmed permanent. Prediction-API FCM (client.fcm.* on /fcm/*) is unchanged.

fcm.create_subtrader(..., require_category_cap=) is required (True or False). When true, ordinary orders need an explicit asset-class IM cap; subtrader-wide and market caps do not satisfy this. Dedicated liquidation remains allowed. The flag is set at create time and cannot be changed.

FCM members can set per-subtrader initial-margin caps. A cap with no market_ticker applies across all markets; a ticker scopes it to one market. im_cap is a non-negative OrderPrice (fixed-point dollars):

from decimal import Decimal

caps = perps.fcm.risk_controls(subtrader_id="user_desk1")
perps.fcm.update_risk_controls(
    subtrader_id="user_desk1",
    im_cap=Decimal("100.0000"),
    market_ticker="BTC-PERP",
)
perps.fcm.delete_risk_controls(subtrader_id="user_desk1", market_ticker="BTC-PERP")
# asset_class is mutually exclusive with market_ticker
perps.fcm.risk_controls(subtrader_id="user_desk1", asset_class="Crypto")

# Member-set account notional limit (PUT/DELETE are never retried).
# The exchange enforces the smaller of this value and the Kalshi-set limit.
perps.fcm.update_notional_risk_limit(notional_value_risk_limit=Decimal("5000.0000"))
perps.fcm.delete_notional_risk_limit()

risk_controls() also returns notional_limits (admin-set notional caps for the same subtrader). Each FCMSubtraderRiskControls row includes required current_im. margin.notional_risk_limit() reports required total_current_usage and current_usage_by_market_ticker, plus optional member_notional_value_risk_limit and effective_account_notional_value_risk_limit.

Exit triggers (stop-loss / take-profit / trailing) sit on a position slot. Bracket stop_loss_price / take_profit_price must be strictly positive when supplied; the client rejects zero and negative values.

from decimal import Decimal
from kalshi.perps.models.portfolio import SetCrossExitTriggerRequest

listed = perps.portfolio.cross_exit_triggers("BTC-PERP")
trig = perps.portfolio.set_cross_exit_trigger(
    "BTC-PERP",
    request=SetCrossExitTriggerRequest(
        kind="bracket",
        stop_loss_price=Decimal("50000.0000"),
        take_profit_price=Decimal("70000.0000"),
    ),
)
perps.portfolio.cancel_cross_exit_trigger("BTC-PERP", trig.id)

Value types & timestamps

  • Prices are DollarDecimal — FixedPointDollars strings with up to 6 decimal places, parsed to Decimal (no float on the price path).
  • Counts are FixedPointCount — 2-decimal fixed-point with an _fp wire suffix.
  • number/double ratios (margin thresholds, funding rate) use MultiplierDecimal to avoid binary-float drift.
  • REST timestamps are RFC3339 (AwareDatetime); some query/response fields are Unix-seconds int.
  • WebSocket timestamps are Unix epoch milliseconds on *_ms-suffixed fields (ts_ms, created_ts_ms, next_funding_time_ms, …) — a real parsing difference from the event-contract WS.
  • Notional values — MarginMarket (REST) and the margin ticker WS payload carry optional volume_notional_value / volume_24h_notional_value / open_interest_notional_value (DollarDecimal | None); MarginMarketCandlestick carries the same fields as required (inherent to a settled historical record). MarginMarket.leverage_estimates maps notional position sizes ("1000", "10000", …) to MultiplierDecimal leverage, or None when margin config / price data is unavailable. MarginMarket.bid_size / ask_size (FixedPointCount | None) are the resting size at the best bid/ask (bid_size_fp / ask_size_fp on the wire); omitted when market stats are unavailable. MarginMarket.funding_deadband / funding_cap (MultiplierDecimal) and funding_limit_unit ("percentage" / "dollar") are required; they are the funding limits configured at market creation, expressed in funding_limit_unit. MarginMarket.funding_invalid_premium_handling is required ("dampen" / "exclude"): how funding averages a second with no premium (no orderbook sample, no index price, or a trading halt). dampen counts that second as a premium of 0; exclude leaves it out of the average. MarginMarket.halt_sources is optional (list[str] | None; spec-required, None if absent): why the market is halted ("initial_setup", "corporate_action", …). Empty means the market is not halted or the reason has not been observed — do not read empty as proof the market is live.
  • Mark prices (since the v3.21.0 spec sync) — MarginMarket carries three optional reference-price objects: settlement_mark_price, liquidation_mark_price, and reference_price, each a nested TickerPrice (TickerPrice | None). TickerPrice is a new model with price (DollarDecimal, a FixedPointDollars USD string) and ts_ms (the source timestamp in epoch milliseconds, int). All three are absent from the spec required list, so they are None when the upstream price is unavailable.
  • Exchange shard (SDK v11.0.0) — MarginMarket.exchange_index (int) is required; order groups may only reference markets that share their exchange_index.
  • Trading schedule (since the v3.25.0 spec sync / SDK v7.1.0) — MarginMarket carries a required schedule key typed MarginMarketSchedule | None. None means the market trades 24/7. When present, MarginMarketSchedule has is_open: bool plus next_close_ts / next_open_ts (int | None, Unix epoch seconds): next_close_ts is null while closed, next_open_ts is null while open.
  • 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.
  • 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 portfolio level. MarginOrder gained an optional order_reason ("liquidation" / "take_profit_stop_loss") identifying system-generated orders.

Funding mechanics

Funding is the perpetual-futures tether: periodic payments between longs and shorts keep the perp's mark price near its index. The funding resource exposes the current in-progress estimate, historical applied rates, and the authenticated user's per-payment history:

est = perps.funding.rate_estimate(ticker="BTC-PERP")
print(est.funding_rate, est.next_funding_time)        # in-progress estimate
print(est.premium_index, est.premium_index_ts)        # optional final-second premium
points = perps.funding.premium_index(
    ticker="BTC-PERP", start_ts=1_700_000_000, end_ts=1_700_003_600
)
rows = perps.funding.history(start_date="2026-01-01", end_date="2026-02-01")

subscribe_ticker on the WS also carries the live funding_rate and next_funding_time_ms.

Risk & balance introspection

bal = perps.margin.balance(compute_available_balance=True)
for sub in bal.subaccount_balances:
    print(sub.subaccount, sub.account_equity, sub.maintenance_margin)

risk = perps.margin.risk()
print(risk.account_leverage, risk.total_maintenance_margin)
for pos in risk.positions:
    print(pos.market_ticker, pos.position_leverage, pos.estimated_liquidation_price)

perps.margin.api_limits() (GET /account/limits/perps) returns the Perps API tier limits in the same shape as the prediction API's client.account.limits() (an AccountApiLimits with usage_tier, read/write token buckets, and the grants list of ApiUsageLevelGrant).

perps.margin.corporate_actions() (GET /margin/corporate_actions) lists scheduled notices and notices updated in the last seven days. Optional ticker / market_id filters; omit both for all margin markets. Public market data (no auth). Scheduled, applied, and lapsed notices include exactly one terms object matching type (cash, split, final_settlement, or ticker_change). Cancelled notices omit terms and execution_time. Amounts are DollarDecimal US dollar strings.

notices = perps.margin.corporate_actions(ticker="AAPL-PERP")
for action in notices.corporate_actions:
    print(action.id, action.type, action.status)
    if action.cash is not None:
        print(action.cash.cash_per_contract, action.cash.ex_date)

WebSocket streaming

from kalshi import PerpsWebSocket
from kalshi.perps import PerpsConfig

ws = PerpsWebSocket(auth=perps._auth, config=PerpsConfig.demo())
async with ws.connect() as session:
    async for msg in await session.subscribe_ticker(tickers=["BTC-PERP"]):
        if msg.msg.funding_rate is not None:
            print(msg.msg.funding_rate.rate, msg.msg.funding_rate.next_funding_time_ms)

Eight data channels — orderbook_delta (snapshot + delta, sequenced), ticker, trade, fill, user_orders, order_group_updates, corporate_actions, market_lifecycle_v2. The connection, sequence-gap detection, reconnect, and backpressure machinery are reused from the event-contract WS stack (see WebSocket); the perps orderbook is bid / ask. The prediction-only channels (market_positions, multivariate*, communications, cfbenchmarks_value) have no perps counterpart. corporate_actions streams applied split / final-settlement / ticker-change notices (cash actions are 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.

Self-Clearing-Member "Klear" API

The Klear API (settlement balances, obligations, margin reports, withdrawals) is a third surface with a different auth model — a pre-generated Bearer token passed as Authorization: Bearer <admin_user_id>:<access_token> on every request. Generate the token and find your admin user id at https://klearing.kalshi.com (the "Security" page). It is exposed via KlearClient / AsyncKlearClient:

from kalshi import KlearClient

with KlearClient(admin_user_id="...", access_token="...", demo=True) as klear:
    reports = klear.margin.margin_reports(start_date="2026-01-01", end_date="2026-02-01")
    bal = klear.margin.settlement_balance()

Credentials can also come from the environment via KlearClient.from_env() (reads KALSHI_KLEAR_ADMIN_USER_ID / KALSHI_KLEAR_ACCESS_TOKEN).

klear.margin.active_obligations() returns all currently-active settlement obligations. klear.margin.settlement_estimate_by_asset_class() returns next-settlement estimates keyed by asset class. (The singular active_obligation() / settlement_estimate() endpoints were removed upstream in the v11.0.0 reconcile.)

AssetClassLiteral is "Crypto", "Equities", "Metals", "Indices", or "Energy". klear.margin.settlement_prices(asset_class="Crypto", settlement_time=...) returns a ticker → centicents map at a settlement cycle. klear.margin.estimate_maintenance_margin(asset_class="Crypto", positions=[...]) margins a hypothetical portfolio. Optional date= (YYYY-MM-DD) and clearing_type= ("FCM" / "SelfClearing") select the matrix day and clearing arrangement. klear.margin.estimate_maintenance_margin_metadata(asset_class="Crypto", date=...) returns the matrices, liquidation configs, and subgroups that feed that estimate.

klear.margin.funding_estimate_by_asset_class() returns the next-funding estimate keyed by asset class. klear.margin.funding_schedule(asset_class="Crypto") returns a cron expression evaluated in US Eastern Time.

klear.margin.member_funding_payments(funding_time=...) returns the member's funding payments for one funding execution (distinct from the obligation-scoped funding_payments(obligation_id)).

FCM-bound margin API keys:

keys = klear.margin.list_fcm_api_keys()
created = klear.margin.create_fcm_api_key(
    name="desk", public_key=pem, fcm_subtrader_id="user_desk1"
)
klear.margin.delete_fcm_api_key(created.api_key_id)

When an ObligationEntry inline detail array is capped at 1000 rows, the matching *_truncated flag is set; page the full set via the methods below.

corporate_action_payments is required and may be empty. Those rows are cash and split corporate-action payments already included in settlement PnL — do not add them to the obligation total. CorporateActionPaymentDetail.payment_time is always present and is null for splits. Centicents fields on that model stay plain integers. payment_amount_fp is the optional dollar-string counterpart of payment_amount_centicents (separate attribute, not an alias). position_quantity_fp and post_position_quantity_fp are fixed-point contract counts. corporate_action_payments / corporate_action_payments_all page the full set when corporate_action_payments_truncated is true (limit max 1000):

for row in klear.margin.settlement_details_all(ob.id):
    ...
for row in klear.margin.maintenance_margin_details_all(ob.id):
    ...
for row in klear.margin.funding_payments_all(ob.id):
    ...
for row in klear.margin.corporate_action_payments_all(ob.id):
    ...

Each AssetClassSettlementEstimate exposes optional omitted_subtrader_count / group_breakdowns / omitted_group_count. MaintenanceMarginDetail may include margin_group_id.

Subtrader groups — margined as one netted portfolio:

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), plain int on the Python models. Where the spec also publishes a *_fp counterpart (the same amount as a fixed-point dollar string, 6 decimal places), the SDK exposes it as a separate optional DollarDecimal attribute with that *_fp name. None means the key was absent. There is no aliasing: a payload can carry both the integer and the dollar string, and model_dump(by_alias=True) emits both names. The withdrawal amount, GetSettlementBalanceWithdrawalResponse.amount, and MarketSettlementEstimate.session_avg_price_fp are fixed-point dollar strings that were never centicents. klear.margin.withdraw_settlement_balance(amount="500.00") posts to POST /margin/settlement_balance_withdrawal and validates the amount as positive at construction (the single real-money write) before any request is sent. Pass mfa_code= when the clearing member requires MFA. The Bearer access_token is never logged and is redacted in repr() (only the non-secret admin_user_id is shown).

Perps over FIX

The margin product is also reachable over the FIX protocol via MarginFixClient (from kalshi import MarginFixClient), which reuses the shared FIX core with the KALSHI_PERPS_* key and fixed-point-dollar pricing. Margin supports the order-entry, drop-copy, and market-data sessions (no RFQ / post-trade). See FIX protocol.