diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cec266..ad32c7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/kalshi/models/communications.py b/kalshi/models/communications.py index 21facbd..6db862c 100644 --- a/kalshi/models/communications.py +++ b/kalshi/models/communications.py @@ -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"} @@ -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"} diff --git a/kalshi/models/orders.py b/kalshi/models/orders.py index d84a6f7..9b7e5ca 100644 --- a/kalshi/models/orders.py +++ b/kalshi/models/orders.py @@ -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 @@ -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"} diff --git a/kalshi/models/subaccounts.py b/kalshi/models/subaccounts.py index 82400cb..ed4761a 100644 --- a/kalshi/models/subaccounts.py +++ b/kalshi/models/subaccounts.py @@ -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"} @@ -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"} diff --git a/kalshi/resources/subaccounts.py b/kalshi/resources/subaccounts.py index 4080daa..1ebfb83 100644 --- a/kalshi/resources/subaccounts.py +++ b/kalshi/resources/subaccounts.py @@ -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). """ diff --git a/tests/test_subaccounts.py b/tests/test_subaccounts.py index a09ebb6..38bacaa 100644 --- a/tests/test_subaccounts.py +++ b/tests/test_subaccounts.py @@ -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")