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
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,44 @@ All notable changes to kalshi-sdk will be documented in this file.

## Unreleased

### Contract-map completeness (#171)

Maps the remaining 42 REST sub-models, V2 orders family, and internal
containers into `CONTRACT_MAP` (49 entries → 91). Promotes
`test_contract_map_completeness` from `warnings.warn` to `pytest.fail` so
the next unmapped model fails CI loudly.

`_get_schema_fields` / `_get_required_fields` gain a dotted-path syntax
(`Parent.field.items`) so inline-object schemas the spec doesn't name at the
top level (`Batch*OrdersV2*` per-entry shapes) can still flow through the
drift pipeline.

Newly-surfaced drift caught by mapping these models:

- `BidAskDistribution` (OHLC): all four price fields tightened to required.
- `PriceDistribution`: gains 4 v3.18.0 spec fields (`mean_dollars`,
`previous_dollars`, `min_dollars`, `max_dollars`), all optional per spec.
- `Candlestick`: 6 fields tightened to required.
- `MarketMetadata`: `image_url` + `color_code` tightened.
- `Schedule` / `WeeklySchedule`: tightened.
- `PositionsResponse`, `EventCandlesticks`, `ForecastPercentilesPoint`:
tightened.
- `AssociatedEvent` (multivariate): `is_yes_only` + `active_quoters` tightened.
- `LookupPoint` (multivariate): `selected_markets` + `last_queried_ts` tightened.

`OrderbookLevel` is mapped to spec's `PriceLevelDollarsCountFp`, a positional
2-tuple `["<dollars_string>", "<fp_count_string>"]`. The SDK wraps it as a
named `{price, quantity}` object — no field-by-field comparison possible.
`_get_schema_fields` returns `{}` for the array-typed spec schema, so drift
checks skip it cleanly.

Fixture builders for `Candlestick`, `BidAskDistribution`, `PriceDistribution`
added to `tests/_model_fixtures.py` (3 new). Test fixtures parsing
`Candlestick` / `EventCandlesticks` / `MarketCandlesticks` now use those
builders.

### Required-but-optional drift closure (#172)

Required-but-optional drift closure (#172). Drops `None` defaults on 226
spec-required Pydantic model fields across 34 response models (21 REST, 13
WS). The SDK now matches the OpenAPI v3.18.0 / AsyncAPI v0.14 `required` set
Expand Down
204 changes: 196 additions & 8 deletions kalshi/_contract_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,14 +252,202 @@ class ContractEntry:
sdk_model="kalshi.models.order_groups.CreateOrderGroupResponse",
spec_schema="CreateOrderGroupResponse",
),
# Intentionally excluded from contract map:
# - Candlestick, BidAskDistribution, PriceDistribution, OrderbookLevel:
# Nested/composite models, no direct 1:1 spec schema match
# - DailySchedule, WeeklySchedule, MaintenanceWindow, Schedule:
# Simple sub-schemas validated through parent model tests
# - PositionsResponse: SDK-internal container shape
# - MarketMetadata, SettlementSource: Simple sub-schemas
# - Page[T]: SDK-internal pagination wrapper
# -- markets sub-models (#171) ---------------------------------------
ContractEntry(
sdk_model="kalshi.models.markets.BidAskDistribution",
spec_schema="BidAskDistribution",
notes="OHLC sub-model on Candlestick.yes_bid / .yes_ask",
),
ContractEntry(
sdk_model="kalshi.models.markets.PriceDistribution",
spec_schema="PriceDistribution",
notes="OHLC sub-model on Candlestick.price",
),
ContractEntry(
sdk_model="kalshi.models.markets.Candlestick",
spec_schema="MarketCandlestick",
notes="Spec uses 'MarketCandlestick'; SDK calls it 'Candlestick'",
),
ContractEntry(
sdk_model="kalshi.models.markets.OrderbookLevel",
spec_schema="PriceLevelDollarsCountFp",
notes=(
"Spec encodes price levels as a positional 2-tuple "
"['<dollars_string>', '<fp_count_string>']; the SDK exposes it "
"as a named object with `price` / `quantity`. No field-by-field "
"comparison is meaningful — _get_schema_fields returns {} for "
"this array-typed schema."
),
),
# -- orders V2 family (#171) -----------------------------------------
ContractEntry(
sdk_model="kalshi.models.orders.AmendOrderRequest",
spec_schema="AmendOrderRequest",
notes="Request body for /portfolio/orders/{order_id}/amend",
),
ContractEntry(
sdk_model="kalshi.models.orders.DecreaseOrderRequest",
spec_schema="DecreaseOrderRequest",
notes="Request body for /portfolio/orders/{order_id}/decrease",
),
ContractEntry(
sdk_model="kalshi.models.orders.BatchCreateOrdersRequest",
spec_schema="BatchCreateOrdersRequest",
),
ContractEntry(
sdk_model="kalshi.models.orders.BatchCancelOrdersRequest",
spec_schema="BatchCancelOrdersRequest",
),
ContractEntry(
sdk_model="kalshi.models.orders.BatchCancelOrdersRequestOrder",
spec_schema="BatchCancelOrdersRequest.orders.items",
notes="Inline object schema (no named component)",
),
ContractEntry(
sdk_model="kalshi.models.orders.CreateOrderV2Request",
spec_schema="CreateOrderV2Request",
),
ContractEntry(
sdk_model="kalshi.models.orders.CreateOrderV2Response",
spec_schema="CreateOrderV2Response",
),
ContractEntry(
sdk_model="kalshi.models.orders.AmendOrderV2Request",
spec_schema="AmendOrderV2Request",
),
ContractEntry(
sdk_model="kalshi.models.orders.AmendOrderV2Response",
spec_schema="AmendOrderV2Response",
),
ContractEntry(
sdk_model="kalshi.models.orders.DecreaseOrderV2Request",
spec_schema="DecreaseOrderV2Request",
),
ContractEntry(
sdk_model="kalshi.models.orders.DecreaseOrderV2Response",
spec_schema="DecreaseOrderV2Response",
),
ContractEntry(
sdk_model="kalshi.models.orders.CancelOrderV2Response",
spec_schema="CancelOrderV2Response",
),
ContractEntry(
sdk_model="kalshi.models.orders.BatchCreateOrdersV2Request",
spec_schema="BatchCreateOrdersV2Request",
),
ContractEntry(
sdk_model="kalshi.models.orders.BatchCreateOrdersV2Response",
spec_schema="BatchCreateOrdersV2Response",
),
ContractEntry(
sdk_model="kalshi.models.orders.BatchCreateOrdersV2ResponseEntry",
spec_schema="BatchCreateOrdersV2Response.orders.items",
notes="Inline object schema (no named component)",
),
ContractEntry(
sdk_model="kalshi.models.orders.BatchCancelOrdersV2Request",
spec_schema="BatchCancelOrdersV2Request",
),
ContractEntry(
sdk_model="kalshi.models.orders.BatchCancelOrdersV2RequestOrder",
spec_schema="BatchCancelOrdersV2Request.orders.items",
notes="Inline object schema (no named component)",
),
ContractEntry(
sdk_model="kalshi.models.orders.BatchCancelOrdersV2Response",
spec_schema="BatchCancelOrdersV2Response",
),
ContractEntry(
sdk_model="kalshi.models.orders.BatchCancelOrdersV2ResponseEntry",
spec_schema="BatchCancelOrdersV2Response.orders.items",
notes="Inline object schema (no named component)",
),
# -- events sub-models (#171) ----------------------------------------
ContractEntry(
sdk_model="kalshi.models.events.MarketMetadata",
spec_schema="MarketMetadata",
),
ContractEntry(
sdk_model="kalshi.models.events.SettlementSource",
spec_schema="SettlementSource",
),
# -- exchange sub-models (#171) --------------------------------------
ContractEntry(
sdk_model="kalshi.models.exchange.Schedule",
spec_schema="Schedule",
),
ContractEntry(
sdk_model="kalshi.models.exchange.DailySchedule",
spec_schema="DailySchedule",
),
ContractEntry(
sdk_model="kalshi.models.exchange.WeeklySchedule",
spec_schema="WeeklySchedule",
),
ContractEntry(
sdk_model="kalshi.models.exchange.MaintenanceWindow",
spec_schema="MaintenanceWindow",
),
# -- portfolio sub-models (#171) -------------------------------------
ContractEntry(
sdk_model="kalshi.models.portfolio.IndexedBalance",
spec_schema="IndexedBalance",
notes="Per-shard balance entry on Balance.balance_breakdown",
),
ContractEntry(
sdk_model="kalshi.models.portfolio.PositionsResponse",
spec_schema="GetPositionsResponse",
notes="Spec uses 'GetPositionsResponse'; SDK exposes as 'PositionsResponse'",
),
ContractEntry(
sdk_model="kalshi.models.portfolio.Deposit",
spec_schema="Deposit",
),
ContractEntry(
sdk_model="kalshi.models.portfolio.Withdrawal",
spec_schema="Withdrawal",
),
# -- series sub-models (#171) ----------------------------------------
ContractEntry(
sdk_model="kalshi.models.series.EventCandlesticks",
spec_schema="GetEventCandlesticksResponse",
notes="Spec wraps event candlesticks in GetEventCandlesticksResponse",
),
ContractEntry(
sdk_model="kalshi.models.series.PercentilePoint",
spec_schema="PercentilePoint",
),
ContractEntry(
sdk_model="kalshi.models.series.ForecastPercentilesPoint",
spec_schema="ForecastPercentilesPoint",
),
# -- multivariate sub-models (#171) ----------------------------------
ContractEntry(
sdk_model="kalshi.models.multivariate.AssociatedEvent",
spec_schema="AssociatedEvent",
),
ContractEntry(
sdk_model="kalshi.models.multivariate.CreateMarketInMultivariateEventCollectionRequest",
spec_schema="CreateMarketInMultivariateEventCollectionRequest",
),
ContractEntry(
sdk_model="kalshi.models.multivariate.CreateMarketResponse",
spec_schema="CreateMarketInMultivariateEventCollectionResponse",
notes="Long-form CreateMarketInMultivariateEventCollectionResponse",
),
ContractEntry(
sdk_model="kalshi.models.multivariate.LookupTickersForMarketInMultivariateEventCollectionRequest",
spec_schema="LookupTickersForMarketInMultivariateEventCollectionRequest",
),
ContractEntry(
sdk_model="kalshi.models.multivariate.LookupTickersResponse",
spec_schema="LookupTickersForMarketInMultivariateEventCollectionResponse",
notes="Spec name is the long-form ...Response; SDK shortens",
),
ContractEntry(
sdk_model="kalshi.models.multivariate.LookupPoint",
spec_schema="LookupPoint",
),
]

# WS payload models → AsyncAPI schema components.
Expand Down
4 changes: 2 additions & 2 deletions kalshi/models/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ class MarketMetadata(BaseModel):
"""Metadata for a market within an event."""

market_ticker: str
image_url: str | None = None
color_code: str | None = None
image_url: str
color_code: str

model_config = {"extra": "allow"}

Expand Down
18 changes: 9 additions & 9 deletions kalshi/models/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ class WeeklySchedule(BaseModel):

start_time: datetime
end_time: datetime
monday: NullableList[DailySchedule] = []
tuesday: NullableList[DailySchedule] = []
wednesday: NullableList[DailySchedule] = []
thursday: NullableList[DailySchedule] = []
friday: NullableList[DailySchedule] = []
saturday: NullableList[DailySchedule] = []
sunday: NullableList[DailySchedule] = []
monday: NullableList[DailySchedule]
tuesday: NullableList[DailySchedule]
wednesday: NullableList[DailySchedule]
thursday: NullableList[DailySchedule]
friday: NullableList[DailySchedule]
saturday: NullableList[DailySchedule]
sunday: NullableList[DailySchedule]

model_config = {"extra": "allow"}

Expand All @@ -56,8 +56,8 @@ class MaintenanceWindow(BaseModel):
class Schedule(BaseModel):
"""Exchange operating schedule."""

standard_hours: NullableList[WeeklySchedule] = []
maintenance_windows: NullableList[MaintenanceWindow] = []
standard_hours: NullableList[WeeklySchedule]
maintenance_windows: NullableList[MaintenanceWindow]

model_config = {"extra": "allow"}

Expand Down
44 changes: 28 additions & 16 deletions kalshi/models/markets.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,20 +164,16 @@ class Orderbook(BaseModel):
class BidAskDistribution(BaseModel):
"""OHLC data for bid/ask prices within a candlestick period."""

open: DollarDecimal | None = Field(
default=None,
open: DollarDecimal = Field(
validation_alias=AliasChoices("open_dollars", "open"),
)
high: DollarDecimal | None = Field(
default=None,
high: DollarDecimal = Field(
validation_alias=AliasChoices("high_dollars", "high"),
)
low: DollarDecimal | None = Field(
default=None,
low: DollarDecimal = Field(
validation_alias=AliasChoices("low_dollars", "low"),
)
close: DollarDecimal | None = Field(
default=None,
close: DollarDecimal = Field(
validation_alias=AliasChoices("close_dollars", "close"),
)

Expand Down Expand Up @@ -206,6 +202,24 @@ class PriceDistribution(BaseModel):
default=None,
validation_alias=AliasChoices("close_dollars", "close"),
)
# v3.18.0 spec additions (#171). All four nullable per spec since the
# candlestick window may contain no trades.
mean: DollarDecimal | None = Field(
default=None,
validation_alias=AliasChoices("mean_dollars", "mean"),
)
previous: DollarDecimal | None = Field(
default=None,
validation_alias=AliasChoices("previous_dollars", "previous"),
)
min: DollarDecimal | None = Field(
default=None,
validation_alias=AliasChoices("min_dollars", "min"),
)
max: DollarDecimal | None = Field(
default=None,
validation_alias=AliasChoices("max_dollars", "max"),
)

model_config = {"extra": "allow", "populate_by_name": True}

Expand All @@ -217,16 +231,14 @@ class Candlestick(BaseModel):
plus volume and open interest as FixedPointCount strings.
"""

end_period_ts: int | None = None
yes_bid: BidAskDistribution | None = None
yes_ask: BidAskDistribution | None = None
price: PriceDistribution | None = None
volume: FixedPointCount | None = Field(
default=None,
end_period_ts: int
yes_bid: BidAskDistribution
yes_ask: BidAskDistribution
price: PriceDistribution
volume: FixedPointCount = Field(
validation_alias=AliasChoices("volume_fp", "volume"),
)
open_interest: FixedPointCount | None = Field(
default=None,
open_interest: FixedPointCount = Field(
validation_alias=AliasChoices("open_interest_fp", "open_interest"),
)

Expand Down
8 changes: 4 additions & 4 deletions kalshi/models/multivariate.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ class AssociatedEvent(BaseModel):
"""An event associated with a multivariate collection."""

ticker: str
is_yes_only: bool = False
is_yes_only: bool
size_max: int | None = None
size_min: int | None = None
active_quoters: NullableList[str] = []
active_quoters: NullableList[str]

model_config = {"extra": "allow"}

Expand Down Expand Up @@ -142,7 +142,7 @@ class LookupPoint(BaseModel):

event_ticker: str
market_ticker: str
selected_markets: NullableList[TickerPair] = []
last_queried_ts: datetime | None = None
selected_markets: NullableList[TickerPair]
last_queried_ts: datetime

model_config = {"extra": "allow"}
Loading
Loading