Skip to content

Commit 45529f3

Browse files
author
worker
committed
fix(orders): reject bool in CreateOrderRequest.buy_max_cost validator
`buy_max_cost` is integer cents, but bool is an `int` subclass. `buy_max_cost=True` was slipping through as the integer 1 (= a 1 cent cap) — the exact failure mode #225 closed for `DollarDecimal` and `FixedPointCount`. A caller who accidentally passed a flag (e.g. `risk_check_passed`, `dry_run`) where cents were expected ended up placing a real order capped at $0.01 with no error. Prepend an `isinstance(v, bool)` rejection before the Decimal/float branches, mirroring the wording/style of the existing rejections. Audited the file: `buy_max_cost` is the only field with a Decimal/float guard, so no other validators need the same patch. Closes #243
1 parent bf4612d commit 45529f3

2 files changed

Lines changed: 56 additions & 3 deletions

File tree

kalshi/models/orders.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,16 +198,26 @@ class CreateOrderRequest(BaseModel):
198198

199199
@field_validator("buy_max_cost", mode="before")
200200
@classmethod
201-
def _reject_decimal_and_float_buy_max_cost(cls, v: object) -> object:
202-
"""Reject Decimal and float inputs on buy_max_cost.
201+
def _reject_decimal_float_and_bool_buy_max_cost(cls, v: object) -> object:
202+
"""Reject Decimal, float, and bool inputs on buy_max_cost.
203203
204204
Spec says integer cents. Accepting Decimal would silently coerce
205205
callers who pass Decimal('5.00') (expecting $5.00 under the old
206206
DollarDecimal semantics) into 5 cents — data corruption with no
207-
error. Reject at the boundary.
207+
error. Bool is an ``int`` subclass, so ``True`` would otherwise
208+
slip through as 1 (= 1 cent cap), the same class of bug #225
209+
closed for ``DollarDecimal`` / ``FixedPointCount``. Reject at
210+
the boundary (#243).
208211
209212
int and int-shaped strings are fine (Pydantic coerces normally).
210213
"""
214+
if isinstance(v, bool):
215+
raise ValueError(
216+
"buy_max_cost must be int (cents), not bool — "
217+
"bool is an int subclass, so True would otherwise slip "
218+
"through as 1 (= 1 cent cap). Pass cents directly "
219+
"(e.g., 500 for $5.00)."
220+
)
211221
if isinstance(v, Decimal):
212222
raise ValueError(
213223
"buy_max_cost must be int (cents), not Decimal. "

tests/test_models.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,49 @@ def test_buy_max_cost_rejects_float(self) -> None:
871871
buy_max_cost=5.0, # type: ignore[arg-type]
872872
)
873873

874+
def test_buy_max_cost_rejects_bool(self) -> None:
875+
"""#243: bool inputs must raise — bool is an ``int`` subclass, so
876+
``True`` would otherwise slip through as ``1`` (= 1 cent cap), a
877+
silent money-risk failure matching the #225 class of bug for
878+
``DollarDecimal`` / ``FixedPointCount``. A caller who passes a flag
879+
(``risk_check_passed``, ``dry_run``) where cents were expected must
880+
get a clear error, not a $0.01-capped order."""
881+
from pydantic import ValidationError
882+
883+
from kalshi.models.orders import CreateOrderRequest
884+
885+
with pytest.raises(ValidationError, match=r"bool"):
886+
CreateOrderRequest(
887+
ticker="MKT",
888+
side="yes",
889+
action="buy",
890+
count=1,
891+
buy_max_cost=True, # type: ignore[arg-type]
892+
)
893+
with pytest.raises(ValidationError, match=r"bool"):
894+
CreateOrderRequest(
895+
ticker="MKT",
896+
side="yes",
897+
action="buy",
898+
count=1,
899+
buy_max_cost=False, # type: ignore[arg-type]
900+
)
901+
902+
def test_buy_max_cost_accepts_plain_int(self) -> None:
903+
"""#243: regression — plain int (cents) still works."""
904+
from kalshi.models.orders import CreateOrderRequest
905+
906+
req = CreateOrderRequest(
907+
ticker="MKT",
908+
side="yes",
909+
action="buy",
910+
count=1,
911+
buy_max_cost=500,
912+
)
913+
assert req.buy_max_cost == 500
914+
assert isinstance(req.buy_max_cost, int)
915+
assert not isinstance(req.buy_max_cost, bool)
916+
874917
def test_buy_max_cost_accepts_int_string(self) -> None:
875918
"""Int-shaped strings are coerced normally (e.g., loading from env/config)."""
876919
from kalshi.models.orders import CreateOrderRequest

0 commit comments

Comments
 (0)