From 3e5cf429ba74e361ff876f9196a9f5f18d02044d Mon Sep 17 00:00:00 2001 From: Jeff West Date: Sun, 17 May 2026 13:11:37 -0500 Subject: [PATCH] polish(types): retype count/size/volume fields to FixedPointCount (#90) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several response-model fields used DollarDecimal even though their wire alias is `_fp` (FixedPointCount). The annotation falsely signaled "dollar amount" for what are semantically share/contract counts. Both validators are byte-identical today — runtime behavior unchanged — but if either parser ever diverges (per-family scale factor, different precision rule) every mistyped field silently breaks. Swapped: - Market: yes_bid_size, yes_ask_size, no_bid_size, no_ask_size, volume, volume_24h, open_interest - Candlestick: volume, open_interest - OrderbookLevel: quantity - Fill: count - Trade: count - MarketPosition: position - EventPosition: total_cost_shares - Settlement: yes_count, no_count - Series: volume Both DollarDecimal and FixedPointCount resolve to `Decimal` at the type level, so user code handling these fields as Decimal is unaffected. Drift tests pass (the spec doesn't constrain SDK-side type names, only schema names + presence). The orderbook_delta.py `side: str → Literal["yes", "no"]` fix mentioned in the issue is in kalshi/ws/ — out of scope for this branch (Wave 3/4 territory). Filed/tracked separately. Closes #90 Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 13 +++++++++++++ kalshi/models/historical.py | 4 ++-- kalshi/models/markets.py | 26 +++++++++++++------------- kalshi/models/orders.py | 2 +- kalshi/models/portfolio.py | 10 +++++----- kalshi/models/series.py | 4 ++-- 6 files changed, 36 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33ee3c5..74a0b07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,19 @@ All notable changes to kalshi-sdk will be documented in this file. ### Changed +- **Count/size/volume response fields retyped from `DollarDecimal` to + `FixedPointCount` (#90).** Fields with `_fp` wire aliases were annotated + as `DollarDecimal` — the type that signals "dollar amount." Runtime + behavior is unchanged (both validators are byte-identical and parse the + same wire strings into `Decimal`), but the annotation now communicates + the correct semantics and prevents silent corruption if either parser + ever diverges. Affected: `Market.{yes_bid_size, yes_ask_size, no_bid_size, + no_ask_size, volume, volume_24h, open_interest}`, `Candlestick.{volume, + open_interest}`, `OrderbookLevel.quantity`, `Fill.count`, `Trade.count`, + `MarketPosition.position`, `EventPosition.total_cost_shares`, + `Settlement.{yes_count, no_count}`, `Series.volume`. Both types resolve + to `Decimal` at the type level, so user code that handles these fields + as `Decimal` is unaffected. - **`*_all()` methods are now unbounded by default.** Previously, internal `_list_all` had an invisible 1000-page safety cap that silently truncated callers iterating beyond ~100k items. The cap is gone; the cursor-repeat diff --git a/kalshi/models/historical.py b/kalshi/models/historical.py index 65e9706..4f328f3 100644 --- a/kalshi/models/historical.py +++ b/kalshi/models/historical.py @@ -7,7 +7,7 @@ from pydantic import AliasChoices, BaseModel, Field -from kalshi.types import DollarDecimal +from kalshi.types import DollarDecimal, FixedPointCount # Single-value enum per spec (MveHistoricalFilterQuery). mypy rejects plain # `str` here even if it holds "exclude" at runtime — pass the literal directly. @@ -30,7 +30,7 @@ class Trade(BaseModel): trade_id: str ticker: str | None = None - count: DollarDecimal | None = Field( + count: FixedPointCount | None = Field( default=None, validation_alias=AliasChoices("count_fp", "count"), ) diff --git a/kalshi/models/markets.py b/kalshi/models/markets.py index 12b1c97..64cbd8d 100644 --- a/kalshi/models/markets.py +++ b/kalshi/models/markets.py @@ -8,7 +8,7 @@ from pydantic import AliasChoices, BaseModel, Field -from kalshi.types import DollarDecimal, NullableList +from kalshi.types import DollarDecimal, FixedPointCount, NullableList MarketStatusLiteral = Literal["unopened", "open", "paused", "closed", "settled"] """Market status filter for GET /markets. Spec: MarketStatusQuery enum.""" @@ -81,31 +81,31 @@ class Market(BaseModel): ) # Size/volume fields (FixedPointCount) - yes_bid_size: DollarDecimal | None = Field( + yes_bid_size: FixedPointCount | None = Field( default=None, validation_alias=AliasChoices("yes_bid_size_fp", "yes_bid_size"), ) - yes_ask_size: DollarDecimal | None = Field( + yes_ask_size: FixedPointCount | None = Field( default=None, validation_alias=AliasChoices("yes_ask_size_fp", "yes_ask_size"), ) - no_bid_size: DollarDecimal | None = Field( + no_bid_size: FixedPointCount | None = Field( default=None, validation_alias=AliasChoices("no_bid_size_fp", "no_bid_size"), ) - no_ask_size: DollarDecimal | None = Field( + no_ask_size: FixedPointCount | None = Field( default=None, validation_alias=AliasChoices("no_ask_size_fp", "no_ask_size"), ) - volume: DollarDecimal | None = Field( + volume: FixedPointCount | None = Field( default=None, validation_alias=AliasChoices("volume_fp", "volume"), ) - volume_24h: DollarDecimal | None = Field( + volume_24h: FixedPointCount | None = Field( default=None, validation_alias=AliasChoices("volume_24h_fp", "volume_24h"), ) - open_interest: DollarDecimal | None = Field( + open_interest: FixedPointCount | None = Field( default=None, validation_alias=AliasChoices("open_interest_fp", "open_interest"), ) @@ -141,12 +141,12 @@ class Market(BaseModel): class OrderbookLevel(BaseModel): """A single price level in the orderbook. - Quantity is DollarDecimal to support fractional contracts - (FixedPointCount strings like ``"100.50"`` from the API). + Quantity is FixedPointCount — fractional contract counts are sent as + FixedPointCount strings like ``"100.50"`` from the API. """ price: DollarDecimal - quantity: DollarDecimal + quantity: FixedPointCount class Orderbook(BaseModel): @@ -217,11 +217,11 @@ class Candlestick(BaseModel): yes_bid: BidAskDistribution | None = None yes_ask: BidAskDistribution | None = None price: PriceDistribution | None = None - volume: DollarDecimal | None = Field( + volume: FixedPointCount | None = Field( default=None, validation_alias=AliasChoices("volume_fp", "volume"), ) - open_interest: DollarDecimal | None = Field( + open_interest: FixedPointCount | None = Field( default=None, validation_alias=AliasChoices("open_interest_fp", "open_interest"), ) diff --git a/kalshi/models/orders.py b/kalshi/models/orders.py index aa2ec87..5f3a160 100644 --- a/kalshi/models/orders.py +++ b/kalshi/models/orders.py @@ -115,7 +115,7 @@ class Fill(BaseModel): side: str | None = None action: str | None = None is_taker: bool | None = None - count: DollarDecimal | None = Field( + count: FixedPointCount | None = Field( default=None, validation_alias=AliasChoices("count_fp", "count"), ) diff --git a/kalshi/models/portfolio.py b/kalshi/models/portfolio.py index bfa41a8..543b627 100644 --- a/kalshi/models/portfolio.py +++ b/kalshi/models/portfolio.py @@ -7,7 +7,7 @@ from pydantic import AliasChoices, BaseModel, Field -from kalshi.types import DollarDecimal, NullableList +from kalshi.types import DollarDecimal, FixedPointCount, NullableList SettlementStatusLiteral = Literal["all", "unsettled", "settled"] """Position settlement status filter for GET /fcm/positions. Spec: settlement_status query enum.""" @@ -43,7 +43,7 @@ class MarketPosition(BaseModel): default=None, validation_alias=AliasChoices("total_traded_dollars", "total_traded"), ) - position: DollarDecimal | None = Field( + position: FixedPointCount | None = Field( default=None, validation_alias=AliasChoices("position_fp", "position"), ) @@ -73,7 +73,7 @@ class EventPosition(BaseModel): default=None, validation_alias=AliasChoices("total_cost_dollars", "total_cost"), ) - total_cost_shares: DollarDecimal | None = Field( + total_cost_shares: FixedPointCount | None = Field( default=None, validation_alias=AliasChoices("total_cost_shares_fp", "total_cost_shares"), ) @@ -113,7 +113,7 @@ class Settlement(BaseModel): ticker: str event_ticker: str | None = None market_result: str | None = None - yes_count: DollarDecimal | None = Field( + yes_count: FixedPointCount | None = Field( default=None, validation_alias=AliasChoices("yes_count_fp", "yes_count"), ) @@ -121,7 +121,7 @@ class Settlement(BaseModel): default=None, validation_alias=AliasChoices("yes_total_cost_dollars", "yes_total_cost"), ) - no_count: DollarDecimal | None = Field( + no_count: FixedPointCount | None = Field( default=None, validation_alias=AliasChoices("no_count_fp", "no_count"), ) diff --git a/kalshi/models/series.py b/kalshi/models/series.py index d917dec..951dd1d 100644 --- a/kalshi/models/series.py +++ b/kalshi/models/series.py @@ -8,7 +8,7 @@ from pydantic import AliasChoices, BaseModel, Field from kalshi.models.markets import Candlestick -from kalshi.types import DollarDecimal, NullableList +from kalshi.types import FixedPointCount, NullableList # Fee type constants (use str fields, not StrEnum, for forward-compat) FEE_TYPE_QUADRATIC = "quadratic" @@ -31,7 +31,7 @@ class Series(BaseModel): fee_type: str = "" fee_multiplier: float = 0.0 additional_prohibitions: NullableList[str] = [] - volume: DollarDecimal | None = Field( + volume: FixedPointCount | None = Field( default=None, validation_alias=AliasChoices("volume_fp", "volume"), )