Skip to content

perps: exchange status, enabled flag & risk parameters resource #389

Closed

Description

@TexasCoding

Epic: part of #387 · Depends on: #388

Summary

Implement the three read-only PERPS "exchange health / access" endpoints on the new standalone PerpsClient / AsyncPerpsClient: GET /margin/exchange/status (is the margin exchange up and trading), GET /margin/enabled (per-member access gate — perps rolls out member-by-member, so this is the smoke test that tells a caller whether they are allowed onto perps at all), and GET /margin/risk_parameters (system-wide liquidation thresholds plus a per-market initial-margin-multiplier map). All three are GET, so all three retry on 429/502/503/504.

The shared perps primitive schemas ExchangeIndex and ExchangeInstance are defined once by the foundation issue in kalshi/perps/models/common.py. This issue (and the transfers/SCM issues) import them — do not redefine.

Endpoints / scope

Method Path operationId Notes
GET /margin/exchange/status GetMarginExchangeStatus No security block in spec — public/unauth. Returns ExchangeStatus. Spec also returns the ExchangeStatus schema on 500/503/504 (server emits the same body shape on degraded states); our transport maps non-2xx to errors as usual, so only the 200 body is parsed.
GET /margin/enabled GetMarginEnabled Auth required (kalshiAccessKey/Signature/Timestamp). Returns MarginEnabledResponse. This is the access-gate smoke test. Guard client-side with _require_auth() so an unauthenticated caller gets AuthRequiredError instead of a server 401.
GET /margin/risk_parameters GetMarginRiskParameters No security block in spec — public/unauth. Returns GetMarginRiskParametersResponse.

No pagination on any of these (no cursor in any response).

Models

Two new files.

kalshi/perps/models/exchange.py

  • ExchangeStatus(BaseModel) — margin exchange operational status. Distinct from kalshi/models/exchange.py:ExchangeStatus; the perps version is slimmer (no schedule/resume-time field). Do NOT reuse the core model.

    • exchange_active: bool (spec required) — "False if the exchange is no longer taking any state changes at all. True unless under maintenance."
    • trading_active: bool (spec required) — "True if trading is currently permitted on the exchange."
    • model_config = {"extra": "allow"} (response model; tolerate additive server fields, matching core convention).
  • MarginEnabledResponse(BaseModel) — perps access gate.

    • enabled: bool (spec required) — "Indicates whether margin trading is enabled for the user."
    • model_config = {"extra": "allow"}.
  • GetMarginRiskParametersResponse(BaseModel) — system-wide risk parameters.

    • liquidation_margin_ratio_threshold: MultiplierDecimal — spec number/format: double, "Margin ratio at which a position is liquidated." Use MultiplierDecimal (from kalshi.types), NOT bare float — the existing convention routes number/double ratio fields through MultiplierDecimal to avoid binary-float drift (see kalshi/types.py docstring and Series.fee_multiplier).
    • queue_entry_margin_ratio_threshold: MultiplierDecimal — spec number/double, "Margin ratio at which a position enters the liquidation queue."
    • initial_margin_multiplier: dict[str, MultiplierDecimal] — spec type: object, additionalProperties: {number/double}, "Map of market ticker to initial margin multiplier. The initial margin requirement is the maintenance margin multiplied by this value." These are NOT FixedPointDollars/FixedPointCount — they are doubles, hence MultiplierDecimal, not DollarDecimal.
    • All three fields are spec-required. model_config = {"extra": "allow"}.
    • No validation_alias needed — short Python names already match the wire field names exactly.

kalshi/perps/models/common.py

Shared perps primitives (owned here, imported by other perps areas):

  • ExchangeInstance(str, Enum) — spec ExchangeInstance enum (type: string). Members:
    • EVENT_CONTRACT = "event_contract"
    • MARGINED = "margined"
    • Description: "The exchange instance type."
  • ExchangeIndex — spec ExchangeIndex is type: integer, default 0, "Identifier for an exchange shard … currently only 0 supported." Expose as a documented type alias ExchangeIndex = int (mirror the lightweight-alias pattern of UnixSecondsTimestamp in kalshi/types.py); a full model is overkill for a bare int. If a future SCM-transfer issue needs it as a request field, it stays a plain int with default=0.

(ExchangeIndex/ExchangeInstance are not referenced by this issue's three endpoints — they live in the foundation common.py module; listed here only for cross-reference.)

No request models in this issue — all three endpoints are GET with no body, so no extra="forbid" request models and no serialization_alias.

Resource methods

New file kalshi/perps/resources/exchange.py, mirroring kalshi/resources/exchange.py (two classes, sync + async). All return-typed against the perps models. Methods take only *, extra_headers: dict[str, str] | None = None (no path/query params).

class PerpsExchangeResource(SyncResource):
    def status(self, *, extra_headers=None) -> ExchangeStatus: ...
    def enabled(self, *, extra_headers=None) -> MarginEnabledResponse: ...
    def risk_parameters(self, *, extra_headers=None) -> GetMarginRiskParametersResponse: ...

class AsyncPerpsExchangeResource(AsyncResource):
    async def status(self, *, extra_headers=None) -> ExchangeStatus: ...
    async def enabled(self, *, extra_headers=None) -> MarginEnabledResponse: ...
    async def risk_parameters(self, *, extra_headers=None) -> GetMarginRiskParametersResponse: ...
  • status → self._get("/margin/exchange/status", ...) → ExchangeStatus.model_validate(data).
  • enabled → call self._require_auth() first (spec requires auth; fail fast with AuthRequiredError), then self._get("/margin/enabled", ...) → MarginEnabledResponse.model_validate(data).
  • risk_parameters → self._get("/margin/risk_parameters", ...) → GetMarginRiskParametersResponse.model_validate(data).

Reuse the existing SyncResource/AsyncResource base (kalshi/resources/_base.py) unchanged — they already carry _get / _require_auth and depend only on the transport, which the perps client supplies. No builtins.list[T] needed here (no list-returning methods), but keep the import builtins convention if any helper returns a list.

Wire the resource onto PerpsClient / AsyncPerpsClient as client.exchange (e.g. self.exchange = PerpsExchangeResource(self._transport)), per the foundation client scaffolding.

Contract-test wiring

Add to METHOD_ENDPOINT_MAP in tests/_contract_support.py (under a new # ── perps exchange ── block). All GET, no request_body_schema:

MethodEndpointEntry(
    sdk_method="kalshi.perps.resources.exchange.PerpsExchangeResource.status",
    http_method="GET",
    path_template="/margin/exchange/status",
),
MethodEndpointEntry(
    sdk_method="kalshi.perps.resources.exchange.PerpsExchangeResource.enabled",
    http_method="GET",
    path_template="/margin/enabled",
),
MethodEndpointEntry(
    sdk_method="kalshi.perps.resources.exchange.PerpsExchangeResource.risk_parameters",
    http_method="GET",
    path_template="/margin/risk_parameters",
),
  • These paths resolve against the perps spec (specs/perps_openapi.yaml), not the core specs/openapi.yaml. The contract harness currently loads exactly one spec via SPEC_FILE / _load_spec() in tests/test_contracts.py. The foundation issue must parameterize the drift suites to also load the perps spec and route perps METHOD_ENDPOINT_MAP entries (and BODY_MODEL_MAP entries) to it — this issue depends on that being in place.
  • No BODY_MODEL_MAP entries (no request bodies).
  • No EXCLUSIONS entries expected: the three GET methods take no path/query params and the spec declares none for them, so TestRequestParamDrift should pass with zero deviations. (If the harness flags the auth-only enabled method for having no params, do NOT add an exclusion — empty-param GETs are already handled by the existing param-drift logic.)

Tests

New file tests/perps/test_exchange.py (create tests/perps/__init__.py if the foundation issue hasn't). Use respx.mock, reuse the conftest RSA-key/auth/config fixtures (adapted to PerpsConfig / perps base URL per foundation), sync + async variants of each.

  • status: happy path — mock 200 with {"exchange_active": true, "trading_active": false}, assert both bools parse; edge case — response carries an extra additive field, assert extra="allow" keeps it and parsing succeeds.
  • enabled: happy path — mock 200 {"enabled": true} with an authed client, assert .enabled is True; error path — unauthenticated client raises AuthRequiredError without issuing an HTTP request (assert the respx route was not called); error path — 401 from server maps to the SDK's unauthorized error type.
  • risk_parameters: happy path — mock 200 with liquidation_margin_ratio_threshold, queue_entry_margin_ratio_threshold, and an initial_margin_multiplier map of 2+ tickers; assert the two thresholds parse as Decimal (not float) and the map values are Decimal keyed by ticker; edge case — empty initial_margin_multiplier map ({}) parses to an empty dict; edge case — a high-precision double like 0.12345678 round-trips without float drift (confirms MultiplierDecimal, not float).
  • Shared primitives: a small test asserting ExchangeInstance("event_contract") / ExchangeInstance("margined") are valid and an unknown value raises.

Acceptance criteria

  • PerpsExchangeResource and AsyncPerpsExchangeResource implement status, enabled, risk_parameters (sync + async).
  • enabled calls _require_auth() before any HTTP I/O.
  • Models ExchangeStatus, MarginEnabledResponse, GetMarginRiskParametersResponse, ExchangeInstance, ExchangeIndex created in kalshi/perps/models/exchange.py + kalshi/perps/models/common.py; number/double fields use MultiplierDecimal.
  • Models + resource classes exported from kalshi/perps/__init__.py and re-exported from kalshi/__init__.py.
  • Resource wired onto PerpsClient.exchange / AsyncPerpsClient.exchange.
  • METHOD_ENDPOINT_MAP updated with the three GET entries; perps spec committed at specs/perps_openapi.yaml and loaded by the (foundation-parameterized) drift suites.
  • uv run mypy kalshi/ strict clean (builtins.list[T] inside resource classes if any list method is added).
  • uv run ruff check . clean.
  • uv run pytest tests/ -v green, including TestRequestParamDrift and TestRequestBodyDrift against the perps spec.

Dependencies

  • foundation (perps package scaffolding: PerpsConfig, PerpsClient/AsyncPerpsClient, perps base URLs, reuse of KalshiAuth + SyncTransport/AsyncTransport, specs/perps_openapi.yaml committed, and the contract-test harness parameterized to load the perps spec).

Activity

  1. added this to the Perps API milestone on Jun 4, 2026
  2. added
    enhancementNew feature or request
    perpsPerps / margin (perpetual futures) API
    on Jun 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestperpsPerps / margin (perpetual futures) API

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions