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
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ tests/
with `_dollars` suffix field names. SDK models use short Python names (`yes_bid`)
with `validation_alias=AliasChoices("yes_bid_dollars", "yes_bid")` to accept both.
`CreateOrderRequest` serializes with `_dollars` suffix via `serialization_alias`.
Verified against OpenAPI spec v3.13.0 on 2026-04-12.
Verified against OpenAPI spec v3.18.0 on 2026-05-18.
- All prices use `Decimal` via the `DollarDecimal` custom Pydantic type.
- Auth signing payload: `str(timestamp_ms) + METHOD + path_only` (path from urlparse,
no query params, no trailing slash).
Expand Down Expand Up @@ -116,14 +116,14 @@ tests/

## Testing

- pytest + pytest-asyncio + respx (httpx mock); 1455 tests collected (1407 passing, 48 skipped without live credentials).
- pytest + pytest-asyncio + respx (httpx mock); ~1920 tests across unit + contract drift suites.
- Use `respx.mock` for HTTP mocking. Generate test RSA keys via conftest.py fixtures.
- New function → write a test. Bug fix → write a regression test. New error path → write a test that triggers it.

## API Reference

- OpenAPI spec: https://docs.kalshi.com/openapi.yaml (v3.13.0, 90+ endpoints)
- AsyncAPI spec: https://docs.kalshi.com/asyncapi.yaml (11 WebSocket channels)
- OpenAPI spec: https://docs.kalshi.com/openapi.yaml (v3.18.0, 85 endpoints)
- AsyncAPI spec: https://docs.kalshi.com/asyncapi.yaml (13 WebSocket channels)
- Base URL: https://api.elections.kalshi.com/trade-api/v2
- Demo URL: https://demo-api.kalshi.co/trade-api/v2
- Auth: RSA-PSS / SHA256 / MGF1(SHA256) / salt_length=DIGEST_LENGTH / base64
Expand Down
39 changes: 35 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ A professional, spec-first Python SDK for the [Kalshi](https://kalshi.com) predi
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Type checked: mypy strict](https://img.shields.io/badge/mypy-strict-blue.svg)](https://mypy.readthedocs.io/)

- **Full coverage** of the Kalshi REST API (89 endpoints across 19 resources) and WebSocket API (11 channels).
- **Full coverage** of the Kalshi REST API (85 endpoints across 19 resources, OpenAPI v3.18.0) and WebSocket API (13 channels).
- **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.
- **Typed end-to-end**: Pydantic v2 models, `mypy --strict` clean, ships `py.typed`. `Literal` types on fixed-enum kwargs.
- **Spec-aligned with drift guards**: hard-fail contract tests catch query, body, and WebSocket payload drift on every commit.
Expand Down Expand Up @@ -141,6 +143,34 @@ client.orders.create(request=CreateOrderRequest(
))
```

### V2 event-market orders

Spec v3.18.0 introduced the V2 family on `/portfolio/events/orders/*` —
event-scoped semantics with single-book `bid`/`ask` sides and fixed-point
dollar prices. Legacy `/portfolio/orders` keeps working and will be
deprecated no earlier than May 6, 2026.

```python
import uuid
from decimal import Decimal
from kalshi import KalshiClient, CreateOrderV2Request

with KalshiClient.from_env() as client:
resp = client.orders.create_v2(request=CreateOrderV2Request(
ticker="EVENT-MKT",
client_order_id=str(uuid.uuid4()), # required + server idempotency key
side="bid", # BookSideLiteral: "bid" | "ask"
count=Decimal("10"),
price=Decimal("0.50"),
time_in_force="good_till_canceled",
self_trade_prevention_type="taker_at_cross",
))
print(resp.order_id, resp.remaining_count, resp.fill_count)
```

The V2 surface is model-only (no kwarg overload); pass a fully-constructed
request model. See [V2 orders docs](https://texascoding.github.io/kalshi-python-sdk/resources/orders/#v2-event-market-orders) for amend/decrease/batch variants.

## WebSocket streaming

```python
Expand All @@ -161,9 +191,10 @@ async def main() -> None:
asyncio.run(main())
```

Available channels (11): `ticker`, `trade`, `orderbook_delta`, `fill`,
`market_positions`, `user_orders`, `order_group`, `market_lifecycle`,
`multivariate`, `multivariate_lifecycle`, `communications`.
Available channels (13): `ticker`, `trade`, `orderbook_delta`, `fill`,
`market_positions`, `user_orders`, `order_group_updates`,
`market_lifecycle_v2`, `multivariate`, `multivariate_market_lifecycle`,
`communications`, `control_frames`, `root`.

## Error handling

Expand Down
17 changes: 16 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@

See `CHANGELOG.md` for full release history.

- **v2.1.0 (2026-05-18)** — OpenAPI sync v3.13.0 → v3.18.0 (`#155`). V2
event-market orders family (`create_v2` / `amend_v2` / `decrease_v2` /
`cancel_v2` + batched variants on `/portfolio/events/orders/*`),
deposit/withdrawal history, account endpoint-cost introspection,
`Balance.balance_dollars` (soft-breaking at construction sites only),
optional `exchange_index` / `user_filter` / `rfq_user_filter` /
`incentive_description` / `post_only` kwargs on existing endpoints.
Also fixes a recurring false-alarm in the weekly spec-drift workflow
by committing `specs/asyncapi.yaml` as a pinned snapshot.
- **v2.0.0 (2026-05-17)** — audit-driven hardening. 30 audit findings closed
across five parallel waves (`#77`–`#106`) plus follow-ups: WebSocket
recv-loop overhaul (5 reconnect races + narrowed exceptions),
Expand Down Expand Up @@ -36,7 +45,13 @@ See `CHANGELOG.md` for full release history.

## Next milestone

Not scoped. Candidates from the v2.0 audit backlog:
Not scoped. Carry-overs from v2.1 and v2.0 audit backlog:

- **Response-side spec drift detection.** v2.1 contract tests cover request
bodies (`TestRequestBodyDrift`) but not response models, which is how
`Balance.balance_dollars` slipped through 5 rounds of review. Add a
`RESPONSE_MODEL_MAP` + walker that asserts spec-required fields exist on
the SDK model.

- Apply `extra="allow"` policy to WS envelope models (`kalshi/ws/models/`)
to mirror the response-model uniformity from `#114`.
Expand Down
8 changes: 8 additions & 0 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ anything with `_dollars` or `yes_price` / `no_price` is `Decimal` dollars.
- **Position** — your aggregate exposure on a market (signed by side).
- **Settlement** — what the exchange paid out when the market resolved.

Orders come in two families:

- **V1** — `/portfolio/orders/*`. Yes/no sides, paired `yes_price` /
`no_price`. Stable surface; deprecation no earlier than May 6, 2026.
- **V2** — `/portfolio/events/orders/*`. Event-scoped, single-book
`bid` / `ask` sides, single `price` field, required `client_order_id`
acting as an idempotency key. Use this for new event-market integrations.

See [Orders](resources/orders.md), [Portfolio](resources/portfolio.md).

## RFQ and Quote
Expand Down
11 changes: 8 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
A professional, spec-first Python SDK for the [Kalshi](https://kalshi.com) prediction
markets API.

- **Full REST coverage** — 89 endpoints across 19 resources, every kwarg drift-tested
against the OpenAPI spec.
- **Full WebSocket coverage** — 11 channels with sequence-gap detection, automatic
- **Full REST coverage** — 85 endpoints across 19 resources (OpenAPI v3.18.0),
every kwarg drift-tested against the spec.
- **V2 event-market orders** — new `create_v2` / `amend_v2` / `decrease_v2` /
`cancel_v2` family on `/portfolio/events/orders/*`. Legacy `/portfolio/orders`
keeps working; deprecation no earlier than May 6, 2026.
- **Funding + cost introspection** — `portfolio.deposits()`,
`portfolio.withdrawals()`, `account.endpoint_costs()`.
- **Full WebSocket coverage** — 13 channels with sequence-gap detection, automatic
reconnection, backpressure strategies, and an in-memory orderbook builder.
- **Sync and async parity** — `KalshiClient` and `AsyncKalshiClient` share one
transport implementation; method names, kwargs, return types, and error behavior
Expand Down
107 changes: 107 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,112 @@
# Migration

## v2.0 → v2.1

v2.1 syncs the SDK to OpenAPI spec v3.18.0. It's **additive at the resource
surface** — eight new endpoints, several new optional kwargs on existing
methods, and one soft-breaking model-construction change called out below.
Code that only consumes the SDK's responses needs no edits; code that
constructs models directly in tests/mocks needs one small update.

### `Balance.balance_dollars` — required, soft-breaking at construction

Spec v3.18.0 adds `balance_dollars: FixedPointDollars` to
`GetBalanceResponse` as a required field. The server now guarantees it, so
callers parsing API responses (`client.portfolio.balance()`) are unaffected.
**But** any code that builds `Balance(...)` directly — typically test mocks
— will hit `ValidationError` until it adds the field.

```python
# v2.0 — broken in v2.1
Balance(balance=50000, portfolio_value=75000, updated_ts=ts)

# v2.1
Balance(
balance=50000,
balance_dollars=Decimal("500.00"), # new required field
portfolio_value=75000,
updated_ts=ts,
)
```

The accompanying optional `balance_breakdown: list[IndexedBalance] | None`
splits the total across exchange shards when present. `IndexedBalance.balance`
is `DollarDecimal` (matching `balance_dollars` units), not cents — same
field name as `Balance.balance` but a different type. Be deliberate when
reading from `balance.balance_breakdown[i].balance`.

### V2 event-market orders

Six new methods on `OrdersResource` / `AsyncOrdersResource` hit the new
`/portfolio/events/orders/*` paths:

- `create_v2(*, request: CreateOrderV2Request)`
- `cancel_v2(order_id, *, subaccount, exchange_index)`
- `amend_v2(order_id, *, request: AmendOrderV2Request, subaccount)`
- `decrease_v2(order_id, *, request: DecreaseOrderV2Request, subaccount)`
- `batch_create_v2(*, request: BatchCreateOrdersV2Request)`
- `batch_cancel_v2(*, request: BatchCancelOrdersV2Request)`

Legacy `/portfolio/orders` keeps working and will be deprecated no earlier
than May 6, 2026. No migration is required to stay on v1 paths. New
event-market trading should target the V2 family for the cleaner shape
(single `bid`/`ask` side, single `price` field, explicit idempotency).

Two important differences from V1:

1. **`client_order_id` is required** on `CreateOrderV2Request` and acts as
the server-side idempotency key. Reusing a value returns the original
order rather than placing a new one. Use a fresh UUID4 per call.
2. **`side` uses `BookSideLiteral` (`"bid"` / `"ask"`)**, not V1's
`SideLiteral` (`"yes"` / `"no"`).

### Spec-driven asymmetry on V2 amend/decrease

`amend_v2` and `decrease_v2` accept `subaccount` as a **resource-method
kwarg** (query param on the wire) but read `exchange_index` from the
**request body**. `cancel_v2` differs again — both are query params there,
because that endpoint has no body. This mirrors the spec exactly:

```python
client.orders.amend_v2(
"ord-1",
subaccount=3, # query param
request=AmendOrderV2Request(
ticker="EVENT-MKT", side="bid",
price=Decimal("0.55"),
count=Decimal("10"),
exchange_index=0, # body field
),
)
```

### New optional kwargs on existing endpoints

All additive — existing call sites keep working:

- `orders.cancel(*, exchange_index)`, `order_groups.delete(*, exchange_index)`
- `communications.list_rfqs(*, user_filter)`, `communications.list_all_rfqs(*, user_filter)`
- `communications.list_quotes(*, user_filter, rfq_user_filter)`, `communications.list_all_quotes(*, user_filter, rfq_user_filter)`. `user_filter="self"` and `rfq_user_filter="self"` are now standalone satisfiers for the server-side filter requirement (previously only `quote_creator_user_id` / `rfq_creator_user_id` worked).
- `incentive_programs.list(*, incentive_description)` and the `list_all` variant.
- `CreateQuoteRequest.post_only` — pass `post_only=True` to `create_quote()`.
- `exchange_index` on order/amend/decrease/batch-cancel request models.

### New endpoints (additive)

- `portfolio.deposits()` / `portfolio.deposits_all()` — deposit history.
- `portfolio.withdrawals()` / `portfolio.withdrawals_all()` — withdrawal history.
- `account.endpoint_costs()` — token costs for endpoints whose cost differs
from the default.

### New public types

Added to `kalshi.*` and `kalshi.models.*`:

- Literals: `BookSideLiteral`, `UserFilterLiteral`, `PaymentStatusLiteral`, `PaymentTypeLiteral`.
- Models: `Deposit`, `Withdrawal`, `IndexedBalance`, `AccountEndpointCosts`, `EndpointTokenCost`, and the V2 request/response family.

---

## v1.x → v2.0

v2.0 is mostly additive (new `max_pages` kwarg, `KalshiConfig.http2`/`limits`,
Expand Down
39 changes: 37 additions & 2 deletions docs/request-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ OpenAPI spec.
| `client.orders.batch_cancel` | `BatchCancelOrdersRequest` (wraps `list[BatchCancelOrdersRequestOrder]`) |
| `client.orders.amend` | `AmendOrderRequest` |
| `client.orders.decrease` | `DecreaseOrderRequest` |
| `client.orders.create_v2` | `CreateOrderV2Request` (V2 event-market — model-only, no kwarg form) |
| `client.orders.amend_v2` | `AmendOrderV2Request` (V2 — model-only) |
| `client.orders.decrease_v2` | `DecreaseOrderV2Request` (V2 — model-only, XOR `reduce_by`/`reduce_to`) |
| `client.orders.batch_create_v2` | `BatchCreateOrdersV2Request` (wraps `list[CreateOrderV2Request]`) |
| `client.orders.batch_cancel_v2` | `BatchCancelOrdersV2Request` (wraps `list[BatchCancelOrdersV2RequestOrder]`) |
| `client.api_keys.create` | `CreateApiKeyRequest` |
| `client.api_keys.generate` | `GenerateApiKeyRequest` |
| `client.communications.create_rfq` | `CreateRFQRequest` |
Expand Down Expand Up @@ -94,12 +99,42 @@ an awkward wire name:

You never type these long names — they're an internal implementation detail.

## V2 surface: model-only

The V2 event-market order endpoints (`create_v2` / `amend_v2` / `decrease_v2`
/ `batch_*_v2`) don't accept individual kwargs — pass a fully-constructed
request model. This is intentional: V2 took the opportunity to drop the
V1 kwarg-overload surface entirely, so the model is the single source of
truth for the payload shape.

```python
import uuid
from decimal import Decimal
from kalshi import CreateOrderV2Request

req = CreateOrderV2Request(
ticker="EVENT-MKT",
client_order_id=str(uuid.uuid4()), # required — idempotency key
side="bid",
count=Decimal("10"),
price=Decimal("0.50"),
time_in_force="good_till_canceled",
self_trade_prevention_type="taker_at_cross",
)
client.orders.create_v2(request=req)
```

`CreateOrderV2Request.client_order_id` is required (V1's was optional) and
the server treats it as an idempotency key — reusing one returns the
original order rather than placing a new one. Generate a fresh UUID4 per
call.

## Cross-field invariants

Some request models enforce relationships **before** the HTTP call:

- `DecreaseOrderRequest` — exactly one of `reduce_by` / `reduce_to` is
required (XOR enforced via a model validator).
- `DecreaseOrderRequest` / `DecreaseOrderV2Request` — exactly one of
`reduce_by` / `reduce_to` is required (XOR enforced via a model validator).
- `AmendOrderRequest` — at least one of `yes_price` / `no_price` / `count`
must be present (enforced in the resource method, before the body is built).

Expand Down
18 changes: 18 additions & 0 deletions docs/resources/account.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Auth required.
| Method | Endpoint |
|---|---|
| `limits()` | `GET /account/limits` |
| `endpoint_costs()` | `GET /account/endpoint_costs` |

## Read tier limits

Expand All @@ -29,6 +30,23 @@ calls.
the production server returns the nested `RateLimit` objects shown above.
The SDK normalizes to the live shape.

## Endpoint costs

New in v2.1.0. Lists API v2 endpoints whose configured token cost differs
from the default — useful for budgeting against your write tier.

```python
costs = client.account.endpoint_costs()
print(costs.default_cost) # int, the baseline (typically 10)
for entry in costs.endpoint_costs:
# EndpointTokenCost: method (str), path (str), cost (int)
print(entry.method, entry.path, entry.cost)
```

Endpoints not present in `endpoint_costs` use `default_cost`. Batch
endpoints typically appear here with a per-item multiplier (e.g.
`POST /portfolio/orders/batched` costs ~10 tokens per order in the batch).

## Reference

::: kalshi.resources.account.AccountResource
Expand Down
Loading
Loading