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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,23 @@ v3.18.0 / v0.14 fields and promotes additive drift to a hard CI failure.
- Required-but-Optional drift stays as warning-only (~204 entries;
separate policy decision).

### Fixed

- Subaccount-number request fields no longer cap at 32. Demo allocates
ephemeral subaccount numbers above 32 (observed: 41), but the SDK was
rejecting them client-side with a ``ValidationError`` because seven
request-side model fields carried a ``le=32`` bound derived from spec
prose. The actual OpenAPI schema defines no upper bound; only the
description text mentions ``1-32``. Affected fields:
``ApplySubaccountTransferRequest.{from,to}_subaccount``,
``UpdateSubaccountNettingRequest.subaccount_number``,
``CreateOrderRequest.subaccount``,
``BatchCancelOrdersV2RequestOrder.subaccount``,
``CreateRFQRequest.subaccount``,
``CreateQuoteRequest.subaccount``.
The ``ge=0`` lower bound is unchanged. Unblocks the
``test_transfer_between_subaccounts`` nightly integration test (#164).

## 2.1.0 — 2026-05-18

OpenAPI spec sync from v3.13.0 → v3.18.0. Adds the V2 event-market
Expand Down
4 changes: 2 additions & 2 deletions kalshi/models/communications.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ class CreateRFQRequest(BaseModel):
)
replace_existing: bool | None = None
subtrader_id: str | None = None
subaccount: int | None = Field(default=None, ge=0, le=32)
subaccount: int | None = Field(default=None, ge=0)

model_config = {"extra": "forbid"}

Expand Down Expand Up @@ -200,7 +200,7 @@ class CreateQuoteRequest(BaseModel):
yes_bid: DollarDecimal
no_bid: DollarDecimal
rest_remainder: bool
subaccount: int | None = Field(default=None, ge=0, le=32)
subaccount: int | None = Field(default=None, ge=0)
post_only: bool | None = None

model_config = {"extra": "forbid"}
Expand Down
4 changes: 2 additions & 2 deletions kalshi/models/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ class CreateOrderV2Request(BaseModel):
post_only: bool | None = None
cancel_order_on_pause: bool | None = None
reduce_only: bool | None = None
subaccount: int | None = Field(default=None, ge=0, le=32)
subaccount: int | None = Field(default=None, ge=0)
order_group_id: str | None = None
exchange_index: int | None = None

Expand Down Expand Up @@ -557,7 +557,7 @@ class BatchCancelOrdersV2RequestOrder(BaseModel):
"""Single entry in BatchCancelOrdersV2Request.orders."""

order_id: str
subaccount: int | None = Field(default=None, ge=0, le=32)
subaccount: int | None = Field(default=None, ge=0)
exchange_index: int | None = None

model_config = {"extra": "forbid"}
Expand Down
14 changes: 9 additions & 5 deletions kalshi/models/subaccounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,17 @@ class ApplySubaccountTransferRequest(BaseModel):

``amount_cents`` is integer cents per spec (matches the ``buy_max_cost``
convention on ``CreateOrderRequest``). Pass ``500`` for $5.00, never
a Decimal. ``from_subaccount`` and ``to_subaccount`` use ``0`` for
the primary account and ``1-32`` for numbered subaccounts.
a Decimal. ``from_subaccount`` / ``to_subaccount`` use ``0`` for the
primary account and a positive integer for numbered subaccounts. The
server is the source of truth for the upper bound: spec describes
``1-32`` in prose but defines no JSON-schema maximum, and demo has
been observed allocating values above 32. The SDK validates only the
lower bound (``ge=0``) so server-assigned numbers always round-trip.
"""

client_transfer_id: UUID
from_subaccount: int = Field(ge=0, le=32)
to_subaccount: int = Field(ge=0, le=32)
from_subaccount: int = Field(ge=0)
to_subaccount: int = Field(ge=0)
amount_cents: int = Field(gt=0)

model_config = {"extra": "forbid"}
Expand Down Expand Up @@ -79,7 +83,7 @@ class SubaccountTransfer(BaseModel):
class UpdateSubaccountNettingRequest(BaseModel):
"""Body for PUT /portfolio/subaccounts/netting."""

subaccount_number: int = Field(ge=0, le=32)
subaccount_number: int = Field(ge=0)
enabled: bool

model_config = {"extra": "forbid"}
Expand Down
4 changes: 3 additions & 1 deletion kalshi/resources/subaccounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ def _build_update_netting_body(
class SubaccountsResource(SyncResource):
"""Sync subaccounts API.

Subaccount 0 is the primary account; 1-32 are numbered subaccounts.
Subaccount 0 is the primary account; positive integers identify numbered
subaccounts (spec prose says ``1-32`` but defines no JSON-schema upper
bound, and demo has been observed allocating numbers above 32).
POST /portfolio/subaccounts spins up the next subaccount with an
empty body (spec takes no request payload).
"""
Expand Down
24 changes: 15 additions & 9 deletions tests/test_subaccounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,15 +190,21 @@ def test_transfer_request_rejects_zero_amount(self) -> None:
amount_cents=0,
)

def test_transfer_request_rejects_out_of_range_subaccount(self) -> None:
with pytest.raises(ValidationError):
ApplySubaccountTransferRequest(
client_transfer_id=_TEST_XFER_ID,
from_subaccount=0,
to_subaccount=33,
amount_cents=100,
)

def test_transfer_request_accepts_subaccount_above_32(self) -> None:
"""Regression guard for #164: demo allocates subaccount numbers above 32.

Spec describes ``1-32`` in prose but defines no JSON-schema maximum,
and an integration test caught the SDK rejecting a server-assigned 41
before the request could leave the client. The SDK validates only the
lower bound (``ge=0``); the server is the source of truth on the upper.
"""
req = ApplySubaccountTransferRequest(
client_transfer_id=_TEST_XFER_ID,
from_subaccount=0,
to_subaccount=41,
amount_cents=100,
)
assert req.to_subaccount == 41
def test_update_netting_request_serializes(self) -> None:
req = UpdateSubaccountNettingRequest(subaccount_number=2, enabled=True)
body = req.model_dump(exclude_none=True, by_alias=True, mode="json")
Expand Down
Loading