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

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

## 17.1.0 — 2026-09-30

Reconciles upstream OpenAPI **3.31.0 → 3.32.0** content drift plus matching
perps and AsyncAPI updates after nightly contract failures (Closes #525).
**Non-breaking**: every addition is an optional parameter or a new method.
Re-vendored `specs/openapi.yaml` (hash
`b8a5c773e8f5bf1f9836fe29dc5449bbded8c8510e11e3471df86e552bbfe507`),
`specs/asyncapi.yaml` (hash
`902d9a36fdf52821eac5577caa1d8f694baa1788144c76b5527731e851089522`), and
`specs/perps_openapi.yaml` (hash
`e3b906ebbb3eb26cbb555b778ee52ef08843f174bc78b0b6d1052dfb3589a6ef`).

### Added

- Optional **`settlement_status`** (`"unsettled"` / `"settled"` / `"all"`) on
`portfolio.positions` and `portfolio.positions_all` (sync + async). This
kwarg was a phantom removed in v0.7.0; OpenAPI 3.32.0 adds it as a real
query param on `GET /portfolio/positions`. Omit it to keep the server
default (`unsettled`).
- Perps **`funding.premium_index(*, ticker, start_ts, end_ts)`** (sync +
async), public `GET /margin/funding_rates/premium_index`. Returns
`list[MarginPremiumIndexPoint]`. `MarginFundingRateEstimate` gains optional
`premium_index` (`Decimal | None`) and `premium_index_ts`
(`datetime | None`).

### Spec notes

- Core OpenAPI `info.version` is **3.32.0**. Still unimplemented on the core
client: `POST /portfolio/intra_exchange_instance_transfer`.
- AsyncAPI and perps AsyncAPI (`specs/perps_asyncapi.yaml`, hash
`e5cc0f026b8e306e917860b870e23c9152d0d782c33137d42698f051a9dbe824`) add
optional `sending_ts_ms` on message envelopes (when Kalshi queued the
frame). Response models keep `extra="allow"` and do not surface the field.

## 17.0.0 — 2026-09-29

Reconciles upstream OpenAPI **3.31.0** content drift plus matching perps and
Expand Down
16 changes: 16 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Migration

## v17.0 → v17.1.0

Reconciles upstream OpenAPI **3.32.0** (Closes #525). **Non-breaking**
additive APIs.

### Added

- `portfolio.positions` / `positions_all(..., settlement_status=)` on the
sync and async clients. Values are `"unsettled"`, `"settled"`, and
`"all"`. This filter was removed in v0.7.0 because it was not in the spec;
OpenAPI 3.32.0 adds it for real. Omit the kwarg to keep the server default
(`unsettled`).
- Perps `funding.premium_index(*, ticker, start_ts, end_ts)` (public, no
auth) plus `MarginPremiumIndexPoint`. `MarginFundingRateEstimate` gains
optional `premium_index` and `premium_index_ts`.

## v16.0 → v17.0.0

Reconciles upstream OpenAPI **3.31.0** content drift plus matching perps and
Expand Down
6 changes: 5 additions & 1 deletion docs/perps.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ async with AsyncPerpsClient.from_env(demo=True) as perps:
| `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()` |
| `funding` | `rate_estimate()`, `historical_rates()`, `history()` |
| `funding` | `rate_estimate()`, `historical_rates()`, `premium_index()`, `history()` |
| `transfers` | `transfer_instance()`, `create_subaccount()`, `transfer_subaccount()` |
| `fcm` | `create_subtrader(subtrader_suffix=...)`; `risk_controls` / `update_risk_controls` / `delete_risk_controls`; `update_notional_risk_limit` / `delete_notional_risk_limit` |

Expand Down Expand Up @@ -185,6 +185,10 @@ user's per-payment history:
```python
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")
```

Expand Down
5 changes: 3 additions & 2 deletions docs/resources/fcm.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ for mp in client.fcm.positions_all(subtrader_id="st_alpha", settlement_status="u
# async: `async for mp in client.fcm.positions_all(...)`
```

`settlement_status` is the FCM-specific kwarg that does **not** exist on
`portfolio.positions()`.
`settlement_status` is also accepted on
[`portfolio.positions`](portfolio.md#positions) / `positions_all` (OpenAPI
3.32.0). Both endpoints default to `unsettled` when the kwarg is omitted.

## Subtrader admin

Expand Down
5 changes: 5 additions & 0 deletions docs/resources/portfolio.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ if bal.balance_breakdown is not None:
resp = client.portfolio.positions(
limit=200,
count_filter="position", # only return rows with non-zero `position` (etc.)
settlement_status="unsettled", # "unsettled" (server default) | "settled" | "all"
ticker="KXPRES-24-DJT",
event_ticker="KXPRES-24",
)
Expand Down Expand Up @@ -109,6 +110,10 @@ for ep in resp.event_positions:
`count_filter` filters which fields the response **includes a row for** —
filtering by `"position"` returns only markets where your position is non-zero.

`settlement_status` (`SettlementStatusLiteral`: `"unsettled"`, `"settled"`,
`"all"`) selects which live positions to return. Omit it to keep the server
default (`unsettled`). Archived positions stay on `GET /historical/positions`.

## Settlements

```python
Expand Down
2 changes: 1 addition & 1 deletion kalshi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,4 +427,4 @@
"Withdrawal",
]

__version__ = "17.0.0"
__version__ = "17.1.0"
8 changes: 8 additions & 0 deletions kalshi/_contract_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,14 @@ class ContractEntry:
sdk_model="kalshi.perps.models.funding.GetMarginFundingHistoryResponse",
spec_schema="GetMarginFundingHistoryResponse",
),
ContractEntry(
sdk_model="kalshi.perps.models.funding.MarginPremiumIndexPoint",
spec_schema="MarginPremiumIndexPoint",
),
ContractEntry(
sdk_model="kalshi.perps.models.funding.GetMarginPremiumIndexResponse",
spec_schema="GetMarginPremiumIndexResponse",
),
# ── perps transfers (#396) ──
ContractEntry(
sdk_model="kalshi.perps.models.transfers.IntraExchangeInstanceTransferResponse",
Expand Down
5 changes: 4 additions & 1 deletion kalshi/models/portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
from kalshi.types import DollarDecimal, FixedPointCount, NullableList, UnixSecondsTimestamp

SettlementStatusLiteral = Literal["all", "unsettled", "settled"]
"""Position settlement status filter for GET /fcm/positions. Spec: settlement_status query enum."""
"""Position settlement status filter for GET /portfolio/positions and GET /fcm/positions.

Spec: ``settlement_status`` query enum.
"""


class IndexedBalance(BaseModel):
Expand Down
4 changes: 4 additions & 0 deletions kalshi/perps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,11 @@
UpdateFCMSubtraderRiskControlsRequest,
)
from kalshi.perps.models.funding import (
GetMarginPremiumIndexResponse,
MarginFundingHistoryEntry,
MarginFundingRate,
MarginFundingRateEstimate,
MarginPremiumIndexPoint,
)
from kalshi.perps.models.margin_account import (
FeeScheduleLiteral,
Expand Down Expand Up @@ -284,6 +286,7 @@
"GetMarginOrderResponse",
"GetMarginOrdersResponse",
"GetMarginPositionsResponse",
"GetMarginPremiumIndexResponse",
"GetMarginReportsResponse",
"GetMarginRiskParametersResponse",
"GetMarginRiskResponse",
Expand Down Expand Up @@ -328,6 +331,7 @@
"MarginOrderbookSnapshotPayload",
"MarginOrdersResource",
"MarginPosition",
"MarginPremiumIndexPoint",
"MarginReport",
"MarginReportTypeLiteral",
"MarginResource",
Expand Down
4 changes: 4 additions & 0 deletions kalshi/perps/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@
UpdateFCMSubtraderRiskControlsRequest,
)
from kalshi.perps.models.funding import (
GetMarginPremiumIndexResponse,
MarginFundingHistoryEntry,
MarginFundingRate,
MarginFundingRateEstimate,
MarginPremiumIndexPoint,
)
from kalshi.perps.models.margin_account import (
FeeScheduleLiteral,
Expand Down Expand Up @@ -147,6 +149,7 @@
"GetMarginOrderResponse",
"GetMarginOrdersResponse",
"GetMarginPositionsResponse",
"GetMarginPremiumIndexResponse",
"GetMarginRiskParametersResponse",
"GetMarginRiskResponse",
"GetMarginTradesResponse",
Expand All @@ -170,6 +173,7 @@
"MarginOrderbook",
"MarginOrderbookLevel",
"MarginPosition",
"MarginPremiumIndexPoint",
"MarginRiskPosition",
"MarginSubaccountBalance",
"MarginTrade",
Expand Down
45 changes: 40 additions & 5 deletions kalshi/perps/models/funding.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"""Perps (margin) funding models — rate estimate, historical rates, payment history.
"""Perps (margin) funding models — rate estimate, historical rates, premium index, payment history.

Response models for the three read-only perps **funding** endpoints (#395):
Response models for the four read-only perps **funding** endpoints (#395):

- :class:`MarginFundingRate` — a single applied historical funding rate.
- :class:`MarginFundingHistoryEntry` — one row of the authenticated user's
per-payment funding history (the rate joined with the realized payment).
- :class:`MarginFundingRateEstimate` — the current in-progress rate estimate.
- :class:`MarginPremiumIndexPoint` — one per-second informational premium index.

Field-type rules (verified against ``specs/perps_openapi.yaml``):

Expand All @@ -16,9 +17,11 @@
- ``mark_price`` / ``funding_amount`` are ``$ref FixedPointDollars`` strings →
:data:`~kalshi.types.DollarDecimal`.
- ``quantity`` is ``$ref FixedPointCount`` → :data:`~kalshi.types.FixedPointCount`.
- ``funding_time`` / ``computed_time`` / ``next_funding_time`` are RFC3339
``format: date-time`` REST timestamps → :class:`~pydantic.AwareDatetime`
(NOT ``_ms`` epoch ints).
- ``funding_time`` / ``computed_time`` / ``next_funding_time`` /
``premium_index_ts`` / ``second_ts`` are RFC3339 ``format: date-time`` REST
timestamps → :class:`~pydantic.AwareDatetime` (NOT ``_ms`` epoch ints).
- ``premium_index`` is a signed decimal fraction (spec ``type: string``) →
:data:`~kalshi.types.MultiplierDecimal`.

These are response models only — this issue has no request bodies, so every
model uses ``extra="allow"`` (tolerate additive server fields) and never
Expand Down Expand Up @@ -102,6 +105,38 @@ class MarginFundingRateEstimate(BaseModel):
validation_alias=AliasChoices("mark_price_dollars", "mark_price"),
)
next_funding_time: AwareDatetime
# Final-second premium captured with this estimate. Omitted when that
# second has no available premium (spec optional).
premium_index: MultiplierDecimal | None = None
premium_index_ts: AwareDatetime | None = None


class MarginPremiumIndexPoint(BaseModel):
"""Spec ``MarginPremiumIndexPoint`` — one per-second informational premium.

Both properties are spec-required. ``premium_index`` is a signed decimal
fraction of the index price (``"0"`` when no premium was measurable), not
basis points. Informational only — it may not match the premium used in
the actual funding calculation.
"""

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

second_ts: AwareDatetime
premium_index: MultiplierDecimal


class GetMarginPremiumIndexResponse(BaseModel):
"""Spec ``GetMarginPremiumIndexResponse`` — premium-index envelope.

``points`` is spec-required and uses
:data:`~kalshi.types.NullableList`: a missing key raises ``ValidationError``
(surfacing drift), while a ``null`` array coerces to ``[]``.
"""

points: NullableList[MarginPremiumIndexPoint]

model_config = {"extra": "allow"}


class GetMarginHistoricalFundingRatesResponse(BaseModel):
Expand Down
75 changes: 61 additions & 14 deletions kalshi/perps/resources/funding.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
"""Perps funding resource — rate estimate, historical rates, payment history (#395).

Three read-only GET endpoints. ``rate_estimate`` and ``historical_rates`` are
public (no spec ``security`` block); ``history`` requires RSA-PSS auth and is
guarded client-side with ``_require_auth()`` so an unauthenticated caller gets
``AuthRequiredError`` instead of a server 401. All three retry on 429/502/503/504
(GET).

None of the three funding endpoints paginate — ``GetMarginFundingHistoryResponse``
and ``GetMarginHistoricalFundingRatesResponse`` carry a single array property and
**no cursor** — so there are no ``*_all()`` / ``Iterator`` paginators here. The
two list endpoints unwrap their array key and return ``builtins.list[Model]``,
"""Perps funding resource — rate estimate, historical rates, premium index, payment history (#395).

Four read-only GET endpoints. ``rate_estimate``, ``historical_rates``, and
``premium_index`` are public (no spec ``security`` block); ``history`` requires
RSA-PSS auth and is guarded client-side with ``_require_auth()`` so an
unauthenticated caller gets ``AuthRequiredError`` instead of a server 401.
All four retry on 429/502/503/504 (GET).

None of the four funding endpoints paginate —
``GetMarginFundingHistoryResponse``, ``GetMarginHistoricalFundingRatesResponse``,
and ``GetMarginPremiumIndexResponse`` carry a single array property and **no
cursor** — so there are no ``*_all()`` / ``Iterator`` paginators here. The
three list endpoints unwrap their array key and return ``builtins.list[Model]``,
mirroring ``historical.candlesticks``.
"""

Expand All @@ -21,9 +22,11 @@
from kalshi.perps.models.funding import (
GetMarginFundingHistoryResponse,
GetMarginHistoricalFundingRatesResponse,
GetMarginPremiumIndexResponse,
MarginFundingHistoryEntry,
MarginFundingRate,
MarginFundingRateEstimate,
MarginPremiumIndexPoint,
)
from kalshi.resources._base import AsyncResource, SyncResource, _params

Expand All @@ -38,6 +41,16 @@ def _funding_historical_params(
return _params(ticker=ticker, start_ts=start_ts, end_ts=end_ts)


def _premium_index_params(
*,
ticker: str,
start_ts: int,
end_ts: int,
) -> dict[str, Any]:
"""Build query params for ``GET /margin/funding_rates/premium_index``."""
return _params(ticker=ticker, start_ts=start_ts, end_ts=end_ts)


def _funding_history_params(
*,
ticker: str | None,
Expand All @@ -55,7 +68,10 @@ def _funding_history_params(


class FundingResource(SyncResource):
"""Sync perps funding API (``rate_estimate`` / ``historical_rates`` / ``history``)."""
"""Sync perps funding API.

``rate_estimate`` / ``historical_rates`` / ``premium_index`` / ``history``.
"""

def rate_estimate(
self, ticker: str, *, extra_headers: dict[str, str] | None = None
Expand All @@ -81,6 +97,20 @@ def historical_rates(
)
return GetMarginHistoricalFundingRatesResponse.model_validate(data).funding_rates

def premium_index(
self,
*,
ticker: str,
start_ts: int,
end_ts: int,
extra_headers: dict[str, str] | None = None,
) -> builtins.list[MarginPremiumIndexPoint]:
params = _premium_index_params(ticker=ticker, start_ts=start_ts, end_ts=end_ts)
data = self._get(
"/margin/funding_rates/premium_index", params=params, extra_headers=extra_headers
)
return GetMarginPremiumIndexResponse.model_validate(data).points

def history(
self,
*,
Expand All @@ -102,7 +132,10 @@ def history(


class AsyncFundingResource(AsyncResource):
"""Async perps funding API (``rate_estimate`` / ``historical_rates`` / ``history``)."""
"""Async perps funding API.

``rate_estimate`` / ``historical_rates`` / ``premium_index`` / ``history``.
"""

async def rate_estimate(
self, ticker: str, *, extra_headers: dict[str, str] | None = None
Expand All @@ -128,6 +161,20 @@ async def historical_rates(
)
return GetMarginHistoricalFundingRatesResponse.model_validate(data).funding_rates

async def premium_index(
self,
*,
ticker: str,
start_ts: int,
end_ts: int,
extra_headers: dict[str, str] | None = None,
) -> builtins.list[MarginPremiumIndexPoint]:
params = _premium_index_params(ticker=ticker, start_ts=start_ts, end_ts=end_ts)
data = await self._get(
"/margin/funding_rates/premium_index", params=params, extra_headers=extra_headers
)
return GetMarginPremiumIndexResponse.model_validate(data).points

async def history(
self,
*,
Expand Down
Loading
Loading