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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

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

## 22.8.0 — 2026-11-04

Reconciles nightly spec drift (#569) against OpenAPI 3.36.0 and the latest
Klear (SCM) spec. Additive only. No breaking changes.

### Added

- Optional **`netting_enabled`** (`bool | None`) on `MarketPosition`. Spec
3.36.0 marks it required; the SDK keeps it optional (contract exclusion
`server_omits_despite_required`) so payloads omitting the key still parse.
(Closes #569).
- Optional **`settlement_start_ts` / `settlement_end_ts`** (RFC3339
`datetime` or `str`) on Klear `margin.obligation_history` /
`obligation_history_all` (sync and async). Inclusive bounds on settlement
time; omitted keys are not sent.
- Optional **`start_ts` / `end_ts`** (RFC3339 `datetime` or `str`) on Klear
`margin.settlement_balance_history` / `settlement_balance_history_all`
(sync and async). Inclusive bounds on entry creation time; omitted keys
are not sent.

## 22.7.0 — 2026-11-03

Reconciles nightly spec drift (#567) against the latest perps OpenAPI /
Expand Down
8 changes: 7 additions & 1 deletion docs/perps.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,13 @@ Credentials can also come from the environment via `KlearClient.from_env()` (rea
`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
obligations. `klear.margin.obligation_history()` / `obligation_history_all()`
page historical obligations; optional `settlement_start_ts` /
`settlement_end_ts` (RFC3339 `datetime` or `str`) bound settlement time.
`klear.margin.settlement_balance_history()` /
`settlement_balance_history_all()` page settlement-buffer history; optional
`start_ts` / `end_ts` bound entry creation time. Omit a bound to leave it
unfiltered. `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.)
Expand Down
4 changes: 3 additions & 1 deletion docs/resources/portfolio.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,9 @@ are unaffected; constructors/fixtures must pass it.
wire-format aliases — on the wire you see `total_traded_dollars`,
`market_exposure_dollars`, `realized_pnl_dollars`, `fees_paid_dollars`,
`position_fp`. The SDK normalizes to short names returning `Decimal`.
`realized_pnl` is signed.
`realized_pnl` is signed. Optional `netting_enabled` (`bool | None`) reports
whether netting applies to the position's event (`None` when the server
omits the key).

## Reference

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.7.0"
__version__ = "22.8.0"
4 changes: 4 additions & 0 deletions kalshi/models/portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ class MarketPosition(BaseModel):
validation_alias=AliasChoices("fees_paid_dollars", "fees_paid"),
)
last_updated_ts: AwareDatetime
# Spec 3.36.0 marks netting_enabled required. Kept optional (None when
# the key is absent) so payloads and fixtures from before the field
# still validate. See EXCLUSIONS.
netting_enabled: bool | None = None

model_config = {"extra": "allow", "populate_by_name": True}

Expand Down
118 changes: 104 additions & 14 deletions kalshi/perps/klear/resources/margin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
- ``active_obligations`` — ``GET /margin/active_obligations``: all currently-active
obligations.
- ``obligation_history`` / ``obligation_history_all`` — ``GET
/margin/obligation_history``: cursor-paginated (limit max 100).
/margin/obligation_history``: cursor-paginated (limit max 100);
optional ``settlement_start_ts`` / ``settlement_end_ts`` (RFC3339) bound
settlement time.
- ``settlement_details`` / ``settlement_details_all``,
``maintenance_margin_details`` / ``maintenance_margin_details_all``,
``funding_payments`` / ``funding_payments_all``,
Expand All @@ -20,7 +22,8 @@
- ``settlement_balance`` — ``GET /margin/settlement_balance``.
- ``guaranty_fund_balance`` — ``GET /margin/guaranty_fund_balance``.
- ``settlement_balance_history`` / ``settlement_balance_history_all`` — ``GET
/margin/settlement_balance_history``: cursor-paginated (limit max 500).
/margin/settlement_balance_history``: cursor-paginated (limit max 500);
optional ``start_ts`` / ``end_ts`` (RFC3339) bound entry creation time.
- ``withdraw_settlement_balance`` — ``POST /margin/settlement_balance_withdrawal``:
``WithdrawSettlementBalanceRequest`` body; NOT retried (POST).
- ``settlement_balance_withdrawal`` — ``GET /margin/settlement_balance_withdrawal``:
Expand Down Expand Up @@ -117,6 +120,21 @@ def _validate_date_range(start_date: str, end_date: str) -> None:
raise ValueError(f"end_date ({end_date}) must be on or after start_date ({start_date})")


def _rfc3339(value: datetime.datetime | str | None) -> str | None:
"""Coerce a ``datetime`` to RFC3339; pass strings through. ``None`` stays ``None``.

Naive datetimes (no ``tzinfo``) are assumed UTC. Spec requires RFC3339,
which mandates a timezone offset. Strings are forwarded unchanged.
"""
if value is None:
return None
if isinstance(value, datetime.datetime):
if value.tzinfo is None:
value = value.replace(tzinfo=datetime.UTC)
return value.isoformat()
return value


class MarginResource(KlearSyncResource):
"""Sync Klear (SCM) margin API — obligations, estimates, balances, groups."""

Expand Down Expand Up @@ -148,10 +166,21 @@ def obligation_history(
*,
limit: int | None = None,
cursor: str | None = None,
settlement_start_ts: datetime.datetime | str | None = None,
settlement_end_ts: datetime.datetime | str | None = None,
extra_headers: dict[str, str] | None = None,
) -> Page[ObligationEntry]:
"""``GET /margin/obligation_history`` — one cursor-paginated page (limit max 100)."""
params = _params(limit=_validate_limit(limit, hi=100), cursor=cursor)
"""``GET /margin/obligation_history`` — one cursor-paginated page (limit max 100).

``settlement_start_ts`` / ``settlement_end_ts`` are optional RFC3339
bounds on settlement time (``datetime`` or string; omitted when ``None``).
"""
params = _params(
limit=_validate_limit(limit, hi=100),
cursor=cursor,
settlement_start_ts=_rfc3339(settlement_start_ts),
settlement_end_ts=_rfc3339(settlement_end_ts),
)
return self._list(
"/margin/obligation_history",
ObligationEntry,
Expand All @@ -166,11 +195,22 @@ def obligation_history_all(
*,
limit: int | None = None,
max_pages: int | None = None,
settlement_start_ts: datetime.datetime | str | None = None,
settlement_end_ts: datetime.datetime | str | None = None,
extra_headers: dict[str, str] | None = None,
) -> Iterator[ObligationEntry]:
"""Auto-paginate obligation history, yielding each ``ObligationEntry``."""
"""Auto-paginate obligation history, yielding each ``ObligationEntry``.

``settlement_start_ts`` / ``settlement_end_ts`` are optional RFC3339
bounds on settlement time (``datetime`` or string; omitted when ``None``).
"""
_validate_max_pages(max_pages)
params = _params(limit=_validate_limit(limit, hi=100), cursor=None)
params = _params(
limit=_validate_limit(limit, hi=100),
cursor=None,
settlement_start_ts=_rfc3339(settlement_start_ts),
settlement_end_ts=_rfc3339(settlement_end_ts),
)
return self._list_all(
"/margin/obligation_history",
ObligationEntry,
Expand Down Expand Up @@ -388,10 +428,21 @@ def settlement_balance_history(
*,
limit: int | None = None,
cursor: str | None = None,
start_ts: datetime.datetime | str | None = None,
end_ts: datetime.datetime | str | None = None,
extra_headers: dict[str, str] | None = None,
) -> Page[SettlementBalanceHistoryEntry]:
"""``GET /margin/settlement_balance_history`` — one page (limit max 500)."""
params = _params(limit=_validate_limit(limit, hi=500), cursor=cursor)
"""``GET /margin/settlement_balance_history`` — one page (limit max 500).

``start_ts`` / ``end_ts`` are optional RFC3339 bounds on entry
creation time (``datetime`` or string; omitted when ``None``).
"""
params = _params(
limit=_validate_limit(limit, hi=500),
cursor=cursor,
start_ts=_rfc3339(start_ts),
end_ts=_rfc3339(end_ts),
)
return self._list(
"/margin/settlement_balance_history",
SettlementBalanceHistoryEntry,
Expand All @@ -406,11 +457,22 @@ def settlement_balance_history_all(
*,
limit: int | None = None,
max_pages: int | None = None,
start_ts: datetime.datetime | str | None = None,
end_ts: datetime.datetime | str | None = None,
extra_headers: dict[str, str] | None = None,
) -> Iterator[SettlementBalanceHistoryEntry]:
"""Auto-paginate settlement-balance history, yielding each entry."""
"""Auto-paginate settlement-balance history, yielding each entry.

``start_ts`` / ``end_ts`` are optional RFC3339 bounds on entry
creation time (``datetime`` or string; omitted when ``None``).
"""
_validate_max_pages(max_pages)
params = _params(limit=_validate_limit(limit, hi=500), cursor=None)
params = _params(
limit=_validate_limit(limit, hi=500),
cursor=None,
start_ts=_rfc3339(start_ts),
end_ts=_rfc3339(end_ts),
)
return self._list_all(
"/margin/settlement_balance_history",
SettlementBalanceHistoryEntry,
Expand Down Expand Up @@ -759,10 +821,17 @@ async def obligation_history(
*,
limit: int | None = None,
cursor: str | None = None,
settlement_start_ts: datetime.datetime | str | None = None,
settlement_end_ts: datetime.datetime | str | None = None,
extra_headers: dict[str, str] | None = None,
) -> Page[ObligationEntry]:
"""Async :meth:`MarginResource.obligation_history`."""
params = _params(limit=_validate_limit(limit, hi=100), cursor=cursor)
params = _params(
limit=_validate_limit(limit, hi=100),
cursor=cursor,
settlement_start_ts=_rfc3339(settlement_start_ts),
settlement_end_ts=_rfc3339(settlement_end_ts),
)
return await self._list(
"/margin/obligation_history",
ObligationEntry,
Expand All @@ -777,11 +846,18 @@ def obligation_history_all(
*,
limit: int | None = None,
max_pages: int | None = None,
settlement_start_ts: datetime.datetime | str | None = None,
settlement_end_ts: datetime.datetime | str | None = None,
extra_headers: dict[str, str] | None = None,
) -> AsyncIterator[ObligationEntry]:
"""Returns an async iterator over obligation history — use ``async for``."""
_validate_max_pages(max_pages)
params = _params(limit=_validate_limit(limit, hi=100), cursor=None)
params = _params(
limit=_validate_limit(limit, hi=100),
cursor=None,
settlement_start_ts=_rfc3339(settlement_start_ts),
settlement_end_ts=_rfc3339(settlement_end_ts),
)
return self._list_all(
"/margin/obligation_history",
ObligationEntry,
Expand Down Expand Up @@ -992,10 +1068,17 @@ async def settlement_balance_history(
*,
limit: int | None = None,
cursor: str | None = None,
start_ts: datetime.datetime | str | None = None,
end_ts: datetime.datetime | str | None = None,
extra_headers: dict[str, str] | None = None,
) -> Page[SettlementBalanceHistoryEntry]:
"""Async :meth:`MarginResource.settlement_balance_history`."""
params = _params(limit=_validate_limit(limit, hi=500), cursor=cursor)
params = _params(
limit=_validate_limit(limit, hi=500),
cursor=cursor,
start_ts=_rfc3339(start_ts),
end_ts=_rfc3339(end_ts),
)
return await self._list(
"/margin/settlement_balance_history",
SettlementBalanceHistoryEntry,
Expand All @@ -1010,11 +1093,18 @@ def settlement_balance_history_all(
*,
limit: int | None = None,
max_pages: int | None = None,
start_ts: datetime.datetime | str | None = None,
end_ts: datetime.datetime | str | None = None,
extra_headers: dict[str, str] | None = None,
) -> AsyncIterator[SettlementBalanceHistoryEntry]:
"""Returns an async iterator over settlement-balance history — use ``async for``."""
_validate_max_pages(max_pages)
params = _params(limit=_validate_limit(limit, hi=500), cursor=None)
params = _params(
limit=_validate_limit(limit, hi=500),
cursor=None,
start_ts=_rfc3339(start_ts),
end_ts=_rfc3339(end_ts),
)
return self._list_all(
"/margin/settlement_balance_history",
SettlementBalanceHistoryEntry,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "kalshi-sdk"
version = "22.7.0"
version = "22.8.0"
description = "A professional Python SDK for the Kalshi prediction markets and Perps (margin) APIs"
readme = "README.md"
license = { text = "MIT" }
Expand Down
5 changes: 3 additions & 2 deletions specs/asyncapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4426,7 +4426,8 @@ components:
Identifier of the RFQ batch this RFQ is a child of. Omitted for an
RFQ outside a batch. A child can be quoted like a single RFQ, with
these differences: a quote counts only for 30 seconds after it was
sent, and resending an unchanged bid keeps its time priority;
sent, and resending an unchanged bid keeps its time priority only
within those 30 seconds;
nothing executes until every winning maker of the batch has
confirmed, so a confirmed quote can still close without a fill;
an accepted quote can't be cancelled, and not confirming it lapses
Expand Down Expand Up @@ -4569,7 +4570,7 @@ components:
properties:
quote_id:
type: string
description: Unique identifier for the quote
description: Unique identifier of the quote that executes. It can differ from the quote the RFQ creator named
rfq_id:
type: string
description: Identifier of the RFQ this quote is for
Expand Down
20 changes: 15 additions & 5 deletions specs/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.0.0
info:
title: Kalshi Trade API Manual Endpoints
version: 3.35.0
version: 3.36.0
description: Manually defined OpenAPI spec for endpoints being migrated to spec-first approach

servers:
Expand Down Expand Up @@ -2564,7 +2564,10 @@ paths:
put:
operationId: AcceptRFQQuote
summary: Accept RFQ Quote
description: ' Endpoint for accepting a quote scoped to its RFQ. This will require the quoter to confirm.'
description: >-
Endpoint for accepting a quote scoped to its RFQ. This will require the quoter to confirm.
The quote that fills can be another maker's quote at the same price; see
[RFQs](/getting_started/rfqs#flow).
tags:
- communications
security:
Expand Down Expand Up @@ -3054,8 +3057,11 @@ paths:
operationId: GetQuotes
summary: Get Quotes
description: >-
List quotes. The rfq_id filter requires an RFQ UUID. Pass pagination
cursors back unchanged; malformed RFQ IDs or cursors return HTTP 400.
List quotes. The rfq_id filter requires an RFQ UUID. With rfq_id, quotes
are sorted on the RFQ's main side (no for a target-cost RFQ, yes
otherwise): the best price first, then the earliest place in line at that
price. Pass pagination cursors back unchanged; malformed RFQ IDs or
cursors return HTTP 400.
tags:
- communications
security:
Expand Down Expand Up @@ -3262,7 +3268,7 @@ paths:
operationId: AcceptQuote
summary: Accept Quote
deprecated: true
description: 'DEPRECATED: Use PUT /communications/rfqs/{rfq_id}/quotes/{quote_id}/accept instead. Endpoint for accepting a quote. This will require the quoter to confirm.'
description: 'DEPRECATED: Use PUT /communications/rfqs/{rfq_id}/quotes/{quote_id}/accept instead.'
x-mint:
content: |
<Warning>
Expand Down Expand Up @@ -7815,6 +7821,7 @@ components:
- realized_pnl_dollars
- fees_paid_dollars
- last_updated_ts
- netting_enabled
properties:
ticker:
type: string
Expand All @@ -7841,6 +7848,9 @@ components:
type: string
format: date-time
description: Last time the position is updated
netting_enabled:
type: boolean
description: Whether netting applies to this position's event. Set at the subaccount's first order in the event.

EventPosition:
type: object
Expand Down
4 changes: 4 additions & 0 deletions specs/perps_scm_openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ paths:
parameters:
- { name: limit, in: query, required: false, schema: { type: integer, default: 20, maximum: 100 }, description: Number of results per page. }
- { name: cursor, in: query, required: false, schema: { type: string }, description: Opaque cursor from a previous response to fetch the next page. }
- { name: settlement_start_ts, in: query, required: false, schema: { type: string, format: date-time }, description: Inclusive lower bound on settlement time. }
- { name: settlement_end_ts, in: query, required: false, schema: { type: string, format: date-time }, description: Inclusive upper bound on settlement time. }
responses:
'200':
description: Successful response
Expand Down Expand Up @@ -425,6 +427,8 @@ paths:
parameters:
- { name: limit, in: query, required: false, schema: { type: integer, default: 100, maximum: 500 }, description: Number of results per page. }
- { name: cursor, in: query, required: false, schema: { type: string }, description: Cursor from a previous response to fetch the next page. }
- { name: start_ts, in: query, required: false, schema: { type: string, format: date-time }, description: Inclusive lower bound on entry creation time. }
- { name: end_ts, in: query, required: false, schema: { type: string, format: date-time }, description: Inclusive upper bound on entry creation time. }
responses:
'200':
description: Successful response
Expand Down
Loading
Loading