feat(models): backfill v3.18.0 fields on Market / Order / Fill (PR1)#166
Conversation
Response-side spec drift hardening, PR1 of the stack (#157). Adds 23 optional fields the spec gained in v3.18.0 across the three highest- traffic response models. Eliminates the three `test_additive_drift` warnings for these models without changing existing field semantics. Fields added ------------ `Market` (+11): custom_strike (dict[str, Any]), early_close_condition (str), exchange_index (int), fee_waiver_expiration_time (datetime), functional_strike (str), is_provisional (bool), mve_collection_ticker (str), mve_selected_legs (list[dict]), price_level_structure (str), price_ranges (list[dict]), primary_participant_key (str) `Order` (+8): outcome_side (SideLiteral), book_side (BookSideLiteral), last_update_time (datetime), self_trade_prevention_type (SelfTradePreventionTypeLiteral), order_group_id (str), cancel_order_on_pause (bool), subaccount_number (int), exchange_index (int) `Fill` (+4): outcome_side (SideLiteral), book_side (BookSideLiteral), subaccount_number (int), ts (int) Design decisions ---------------- - `mve_selected_legs` and `price_ranges` are typed as `list[dict[str, Any]]` rather than introducing new nested model classes (`MveSelectedLeg`, `PriceRange`). Per the issue: 'Don't invent new Literal aliases in this PR — that's separable polish'; same rationale applies to nested model classes. Spec drift detection only checks property presence, not nested-property typing. - `outcome_side` / `book_side` reuse the existing `SideLiteral` / `BookSideLiteral` aliases defined at the top of `orders.py`. Same for `self_trade_prevention_type` reusing `SelfTradePreventionTypeLiteral`. - `Fill.ts` stays `int | None` (NOT `datetime`). Spec says it's a Unix-ms timestamp; the typed companion is `created_time: datetime`. Auto-coercing would break callers depending on the raw integer. - `Order.subaccount_number` is distinct from the existing `Order.subaccount: int | None`. Both coexist per spec. - All new fields appended at the end of each model with a `v3.18.0 backfill` comment block. No existing fields reordered; append-only matches the spec's evolution pattern. Tests ----- 3 new test classes in `tests/test_models.py` (alongside the existing `TestDollarsAliasFields` / `TestMarketOccurrenceDatetime` precedent; `tests/test_orders.py` is the wrong home — it tests resource HTTP wire shapes, not pure model deserialization): - `TestMarketV3180Fields` (3 tests): scalar fields, object+array fields, defaults-to-None for all 11. - `TestOrderV3180Fields` (4 tests): outcome+book side, remaining 6 fields, subaccount_number-vs-subaccount distinction, defaults-to-None for all 8. - `TestFillV3180Fields` (3 tests): all 4 new fields, ts-is-int-not- datetime regression guard, defaults-to-None for all 4. Verification ------------ uv run pytest tests/test_contracts.py::TestSpecDrift::test_additive_drift[Market,Order,Fill] -W error -> 3 passed (was: 3 failed) uv run pytest tests/ --ignore=tests/integration -q -> 1955 passed, 54 warnings (baseline 1945 + 10 new tests; warnings dropped from 57 to 54 — 3 additive drifts gone) uv run mypy kalshi/ -> Success: no issues uv run ruff check . -> All checks passed! Remaining `Required drift in Market` warning gains two new entries (`price_ranges`, `price_level_structure`) because the spec marks them required but the SDK keeps them `Optional` per the project's pervasive required-but-optional policy. The issue explicitly defers this as separate decision (warn-only, ~204 entries). Closes #159.
Code Review — PR #166: v3.18.0 field backfill (Market / Order / Fill)OverviewAdditive spec-drift fix: 23 new optional response fields on the three highest-traffic models, all matching the v3.18.0 OpenAPI spec. The diff is surgical (3 files, +205/-1) and doesn't touch request models, resource methods, or any existing fields — consistent with the stated append-only policy. What works well
Issues1.
|
Two fixes from claude[bot]'s review (#166): 1. Hoist `Fill` to module-level imports in `tests/test_models.py`. `Market` and `Order` were already top-level; `Fill` was redundantly re-imported inside four test method bodies (one existing, three new in PR1). Now: `from kalshi.models.orders import Fill, Order` once, four inline imports removed. 2. Trim the `# v3.18.0 backfill` comment blocks in markets.py and orders.py. Per CLAUDE.md ("only document the WHY, not the what"): the verbose phrasing partially restated what the field declarations themselves convey. Trimmed each block to 2-3 lines that preserve the non-obvious WHYs: - Market: why list[dict] instead of typed MveSelectedLeg/PriceRange - Order: why outcome_side/book_side coexist with deprecated action/side/is_yes; subaccount_number distinct from subaccount - Fill: why ts stays int (Unix-ms) instead of datetime coercion Extended rationale lives in the PR description where it belongs. Verification: pytest tests/test_models.py -> 79 passed pytest test_additive_drift[Market,Order,Fill] -W error -> 3 passed mypy kalshi/ -> Success ruff check . -> All passed
|
Thanks for the review. Both items addressed in 67623d5. 1.
|
Follow-up Review — revisions in 67623d5Both items from the initial review have been addressed correctly. 1. Fill import hoisted — all four inline 2. Comment blocks trimmed — each block is now 2–3 lines covering only the non-obvious WHY (the No new concerns. This PR is ready to merge. |
Cuts v2.2.0 — the response-side spec drift hardening stack (#157). Version bumps: - pyproject.toml: 2.1.0 → 2.2.0 - kalshi/__init__.py: 2.1.0 → 2.2.0 Docs: - CHANGELOG.md: finalize ## Unreleased → ## 2.2.0 — 2026-05-19 with a release-summary headline. The Added/Changed/Fixed entries written incrementally across #166–#169 stay as-is. - ROADMAP.md: add v2.2.0 to Shipped with a coverage summary; drop the two "Next milestone" items now done by this release (response-side drift detection; WS envelope extra=allow); add two new follow-ups (map remaining REST sub-models into CONTRACT_MAP; required-but- optional drift policy decision). - CLAUDE.md: active milestone → post-v2.2. Release scope (65 new optional fields + warn→fail flip + 1 bugfix): - REST response models: Market +11, Order +8, Fill +4, Event +3, EventMetadata +2, Settlement +1, Trade +2, IncentiveProgram +1, RFQ +1, Quote +3, OrderGroup +1, GetOrderGroupResponse +1, CreateOrderGroupResponse +2 (#166, #167, #168). - WS payload models: 33 fields across 11 payloads — Unix-ms timestamps (`*_ts_ms`), outcome_side/book_side direction encoding, MVE linkage, RFQ/Quote context echoes (#168). - Test infrastructure: additive drift now hard-fails CI; unmapped WS models hard-fail; required-but-optional stays warn-only (~204 entries, separate policy decision). EXCLUSIONS allowlist wired through response-side checks (#165). ErrorPayload registered in WS_CONTRACT_MAP; WS envelopes/helpers all use extra=allow (#168). - Bugfix: dropped le=32 cap on 6 subaccount request fields; demo allocates ephemeral subaccount numbers above 32 (observed 41 in nightly, 44 locally). Spec defines no upper bound (#169). No API breaks. All changes additive or relaxing. Bump is semver-minor. Verification: - uv run pytest tests/ --ignore=tests/integration -q -> 2023 passed, 35 warnings (all required-drift, kept warn-only) - uv run pytest tests/integration/test_subaccounts.py::TestSubaccountsSync::test_transfer_between_subaccounts -> 1 passed against live demo (#164 unblocked end-to-end) - uv run mypy kalshi/ -> Success: no issues found in 76 source files - uv run ruff check . -> All checks passed! - kalshi.__version__ check -> 2.2.0 After merge, push tag v2.2.0 to trigger the release workflow (.github/workflows/release.yml): build sdist+wheel → PyPI trusted- publish → GitHub release with CHANGELOG section as notes.
Closes #159. PR1 of the response-side spec-drift hardening stack (#157). Builds on the EXCLUSIONS infrastructure landed in #165 / #158. Three of the remaining additive-drift warnings eliminated; eight more models (#PR2–#PR4) plus the warn→fail flip (#PR5) still ahead.
What this changes
23 new optional fields across the three highest-traffic response models, matching the v3.18.0 OpenAPI spec.
kalshi/models/markets.py—Market(+11)custom_strikedict[str, Any] | Noneearly_close_conditionstr | Noneexchange_indexint | Nonefee_waiver_expiration_timedatetime | Nonefunctional_strikestr | Noneis_provisionalbool | Nonemve_collection_tickerstr | Nonemve_selected_legslist[dict[str, Any]] | Noneprice_level_structurestr | Noneprice_rangeslist[dict[str, Any]] | Noneprimary_participant_keystr | Nonekalshi/models/orders.py—Order(+8)outcome_sideSideLiteral | Nonebook_sideBookSideLiteral | Nonelast_update_timedatetime | Noneself_trade_prevention_typeSelfTradePreventionTypeLiteral | Noneorder_group_idstr | Nonecancel_order_on_pausebool | Nonesubaccount_numberint | Noneexchange_indexint | Nonekalshi/models/orders.py—Fill(+4)outcome_sideSideLiteral | Nonebook_sideBookSideLiteral | Nonesubaccount_numberint | Nonetsint | NoneDesign decisions
mve_selected_legs/price_rangestyped aslist[dict[str, Any]]instead of introducing newMveSelectedLeg/PriceRangemodel classes. The issue explicitly defers Literal-alias polish to a separate PR; the same rationale applies to nested model classes (spec drift only checks property presence, not nested-property typing). Future PR can promote these to typed models if needed.outcome_side/book_side/self_trade_prevention_typereuse existing*Literalaliases defined at the top oforders.py. No new aliases invented.Fill.tsstaysint | None, NOTdatetime. The spec says it's a Unix-ms timestamp; the typed companion iscreated_time: datetime. Auto-coercing would break callers depending on the raw integer. Test class includes atest_ts_is_int_not_datetimeregression guard.Order.subaccount_numberis distinct fromOrder.subaccount. Both coexist per spec; test verifies they parse and round-trip independently.# v3.18.0 backfillcomment block. No existing fields reordered — append-only matches the spec's evolution pattern and minimizes review diff.No changes to request-body models or resource methods.
Tests
3 new test classes in
tests/test_models.py(10 tests, 23 field assertions). I placed them alongside the existingTestDollarsAliasFields/TestMarketOccurrenceDatetimeprecedent rather than intests/test_orders.py— the latter is for resource HTTP wire-shape tests (TestCreateOrderWireShapeetc.), not pure model deserialization. The issue allowed this discretion ("tests/test_models_markets.py(or whereverMarketis exercised — confirm at edit time)").TestMarketV3180Fields(3 tests): scalar fields, object+array fields (custom_strike,mve_selected_legs,price_ranges), defaults-to-None for all 11.TestOrderV3180Fields(4 tests): outcome+book side, remaining 6 fields,subaccount_number-vs-subaccountcoexistence, defaults-to-None for all 8.TestFillV3180Fields(3 tests): all 4 new fields,ts-is-int-not-datetime regression guard, defaults-to-None for all 4.Verification
Other
test_additive_driftcases (Event, Settlement, Trade, EventMetadata, RFQ, Quote, IncentiveProgram, OrderGroup x3) still warn for un-backfilled models, per the stack plan (#PR2–#PR4).Note on remaining
Required drift in MarketwarningThe required-drift warning for
Marketgains two new entries after this PR (price_ranges,price_level_structure) because the spec marks them required but the SDK keeps themOptionalper the project's pervasive required-but-optional policy. The issue explicitly defers this:This is consistent with how every other DollarDecimal / FixedPointCount field on
Marketis already handled.Part of the response-side spec drift hardening stack (#157).