Repository navigation
Description
Summary
Small model / type quality improvements bundled for a single PR.
Item 1 — WS user_orders and communications payloads use stdlib datetime, not AwareDatetime
#234 standardised REST models on AwareDatetime so a naive RFC3339-without-offset string is rejected. WS payloads use stdlib datetime, which accepts naive values. Pure consistency drift introduced by the partial #234 rollout.
Location: kalshi/ws/models/user_orders.py:3,55-58, kalshi/ws/models/communications.py:3,21,50,82,117,153.
Fix: Replace from datetime import datetime with from pydantic import AwareDatetime and retype the affected fields.
Item 2 — V1 CreateOrderRequest leaves enum-style fields as bare str, V2 uses Literal
# V1
side: str
action: str
time_in_force: str | None = None
self_trade_prevention_type: str | None = None
# V2
side: BookSideLiteral
time_in_force: TimeInForceLiteral
self_trade_prevention_type: SelfTradePreventionTypeLiteralUsers constructing a V1 request directly can pass side="foo" and only fail server-side; V2 callers fail at construction. With V1 deprecation set for no earlier than May 2026, the inconsistency is fresh enough to align before V1 sunset.
Location: kalshi/models/orders.py:163-176 (V1), :459-465 (V2).
Fix: Tighten V1 to Literals (safer call given the spec is stable today).
Item 3 — Optional response fields declared as T | None without default=None
T | None = Field(validation_alias=...) without an explicit default=None makes the field required-but-nullable: silent server omission raises ValidationError. The same modules use default=None on other optional fields, so the omission here looks accidental.
Location: kalshi/models/portfolio.py:67-86 (MarketPosition.total_traded/position), kalshi/models/historical.py:25-35 (Trade.count).
Fix: Audit against OpenAPI spec: where the field is required: true (even if nullable), keep as-is and add a docstring note. Where it isn't required, add default=None.
Item 4 — Timestamp typing inconsistent (int Unix seconds vs AwareDatetime)
Within a single resource, the same conceptual field is int on Balance/Deposit/Withdrawal and AwareDatetime on MarketPosition/Settlement. Users iterating positions and balances side-by-side have to special-case the type.
Location: kalshi/models/portfolio.py:46-49,81,142-153, kalshi/models/subaccounts.py:39-50.
Fix: Introduce a UnixSecondsTimestamp = Annotated[int, ...] alias and use it everywhere a server int64 timestamp lands, or document the convention class-by-class. Greppable wire shape either way.
Item 5 — Decimal NaN survives serialization
f"{Decimal('NaN'):f}" returns "NaN", and _coerce_decimal happily accepts Decimal("NaN") (it short-circuits at the isinstance(value, Decimal) check). A caller doing arithmetic that produces NaN and then constructing CreateOrderRequest(yes_price=that_nan) ships "NaN" on the wire to a real-money order placement endpoint.
Location: kalshi/types.py:36-38.
Fix: In _coerce_decimal, after coercion, call value.is_finite() and raise on NaN/Infinity. One branch protects every Dollar/FP field at parse and at serialise.
Item 6 — MarketLifecyclePayload timestamps lack the _ts_ms naming used elsewhere in v0.14+ backfill
open_ts: int | None = None
close_ts: int | None = None
determination_ts: int | None = None
settled_ts: int | None = NoneIf these are Unix-ms (consistent with sibling channels) the name is misleading; if they are seconds it's a third unit convention. Worth confirming against AsyncAPI and either renaming or annotating.
Location: kalshi/ws/models/market_lifecycle.py:25-31.
Fix: Verify spec units. If ms, rename to open_ts_ms etc. with back-compat aliases. If seconds, add a short docstring noting the unit.
Severity & category
low / polish
Activity
fix(models): AmendOrderRequest Literal narrowing + V1 ge=0 + create()…
fix(ws-models): OrderbookDeltaPayload.ts AwareDatetime + envelope typ…
deps: bump pydantic floor to >=2.4 for money-safety invariants (#346) (…