Skip to content

feat(models): backfill v3.18.0 fields on Market / Order / Fill (PR1)#166

Merged
TexasCoding merged 2 commits into
mainfrom
pr1/backfill-market-order-fill-v3180
May 19, 2026
Merged

feat(models): backfill v3.18.0 fields on Market / Order / Fill (PR1)#166
TexasCoding merged 2 commits into
mainfrom
pr1/backfill-market-order-fill-v3180

Conversation

@TexasCoding

Copy link
Copy Markdown
Owner

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.pyMarket (+11)

Field SDK type
custom_strike dict[str, Any] | None
early_close_condition str | None
exchange_index int | None
fee_waiver_expiration_time datetime | None
functional_strike str | None
is_provisional bool | None
mve_collection_ticker str | None
mve_selected_legs list[dict[str, Any]] | None
price_level_structure str | None
price_ranges list[dict[str, Any]] | None
primary_participant_key str | None

kalshi/models/orders.pyOrder (+8)

Field SDK type
outcome_side SideLiteral | None
book_side BookSideLiteral | None
last_update_time datetime | None
self_trade_prevention_type SelfTradePreventionTypeLiteral | None
order_group_id str | None
cancel_order_on_pause bool | None
subaccount_number int | None
exchange_index int | None

kalshi/models/orders.pyFill (+4)

Field SDK type
outcome_side SideLiteral | None
book_side BookSideLiteral | None
subaccount_number int | None
ts int | None

Design decisions

  • mve_selected_legs / price_ranges typed as list[dict[str, Any]] instead of introducing new MveSelectedLeg / PriceRange model 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_type reuse existing *Literal aliases defined at the top of orders.py. No new aliases invented.
  • Fill.ts stays int | None, NOT datetime. The 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. Test class includes a test_ts_is_int_not_datetime regression guard.
  • Order.subaccount_number is distinct from Order.subaccount. Both coexist per spec; test verifies they parse and round-trip independently.
  • 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 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 existing TestDollarsAliasFields / TestMarketOccurrenceDatetime precedent rather than in tests/test_orders.py — the latter is for resource HTTP wire-shape tests (TestCreateOrderWireShape etc.), not pure model deserialization. The issue allowed this discretion ("tests/test_models_markets.py (or wherever Market is 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-subaccount coexistence, 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] \
                tests/test_contracts.py::TestSpecDrift::test_additive_drift[Order] \
                tests/test_contracts.py::TestSpecDrift::test_additive_drift[Fill] \
                -W error::UserWarning -v
3 passed                                          # was: 3 failed before this PR

$ uv run pytest tests/ --ignore=tests/integration -q
1955 passed, 54 warnings in 127.03s              # baseline 1945 + 10 new; warnings 57 → 54

$ uv run mypy kalshi/                            -> Success: no issues found in 76 source files
$ uv run ruff check .                            -> All checks passed!

Other test_additive_drift cases (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 Market warning

The required-drift warning for Market gains two new entries after this PR (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:

Required-but-Optional drift policy (kept as warn-only; ~204 entries, separate decision).

This is consistent with how every other DollarDecimal / FixedPointCount field on Market is already handled.

Part of the response-side spec drift hardening stack (#157).

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.
@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Code Review — PR #166: v3.18.0 field backfill (Market / Order / Fill)

Overview

Additive 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

  • Correct typing choices. ts: int | None on Fill (not datetime) is the right call and the PR description explains the reasoning clearly. The regression guard assert not isinstance(f.ts, bool) in the test is a nice touch.
  • Literal reuse. SideLiteral, BookSideLiteral, and SelfTradePreventionTypeLiteral are all already defined at the top of orders.py and the values used in tests ("taker_at_cross", "ask", "yes") match the defined literals exactly — no phantom values.
  • _contract_map.py unchanged correctly. Market, Order, and Fill are already registered; the contract drift tests pick up new fields automatically. No registration needed.
  • 10 tests cover all 23 fields, including coexistence of subaccount vs subaccount_number and the ts/datetime distinction.
  • All gates pass — mypy strict, ruff, and the three previously-failing test_additive_drift cases.

Issues

1. Fill imported inside test methods — inconsistent with module-level imports (minor)

test_models.py already imports Market and Order at the top of the file. The three methods in TestFillV3180Fields each repeat from kalshi.models.orders import Fill inside the method body. This is inconsistent and adds visual noise.

# Current — three copies of this inside TestFillV3180Fields methods:
from kalshi.models.orders import Fill

# Fix — add to the module-level imports alongside Order:
from kalshi.models.orders import Fill, Order

2. Inline comments are longer than necessary (style, low priority)

CLAUDE.md guideline: "Only add one [comment] when the WHY is non-obvious." The # v3.18.0 backfill block in each model is justified (the list[dict[str, Any]] rationale and ts/datetime distinction are both non-obvious), but the blocks run 4–5 lines and partially re-explain what the field names already say. The PR description is the right place for the extended rationale; the model files could use a single-line comment per group.

Example trim for markets.py:

# v3.18.0 backfill (#159). mve_selected_legs/price_ranges kept as list[dict] —
# no nested model classes yet; separable polish tracked in a future PR.
custom_strike: dict[str, Any] | None = None
...

This isn't a blocker, but shorter comments age better.


No concerns on

  • Security — response-side deserialization only; extra="allow" was already set on all three models.
  • Performance — optional fields with None defaults have zero overhead when absent.
  • Backwards compatibility — all fields are | None with None defaults; existing callers are unaffected.
  • Contract tests — the three test_additive_drift cases now pass; required-drift entries for price_ranges/price_level_structure are consistent with the project's existing required-but-optional policy.

Summary

One real (minor) fix needed: pull Fill up to the module-level imports in test_models.py. The comment trimming is optional. Everything else looks correct and follows project conventions — good candidate to merge once the import is cleaned up.

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
@TexasCoding

Copy link
Copy Markdown
Owner Author

Thanks for the review. Both items addressed in 67623d5.

1. Fill import hoisted to module level — fixed

Three inline from kalshi.models.orders import Fill in the new test methods + one pre-existing inline in test_fill_accepts_dollars_suffix (which I missed in PR1; cleaning all four together for consistency). All replaced by extending the existing top-level import:

from kalshi.models.orders import Fill, Order

2. Comment blocks trimmed — fixed

You're right that the verbose phrasing partially restated what the field declarations already say. CLAUDE.md is explicit about documenting the WHY, not the WHAT. Trimmed each block to preserve the non-obvious rationale only:

markets.py (5 lines → 3):

# v3.18.0 backfill (#159). mve_selected_legs is list[MveSelectedLeg] and
# price_ranges is list[PriceRange] on the wire — kept as list[dict] here;
# no nested model classes yet (separable polish).

orders.py Order (5 lines → 3):

# v3.18.0 backfill (#159). outcome_side/book_side are the canonical
# direction encoding going forward; deprecated action/side/is_yes stay
# for back-compat. subaccount_number is distinct from subaccount.

orders.py Fill (3 lines → 2):

# v3.18.0 backfill (#159). ts is Unix-ms int per spec — distinct from
# the typed created_time: datetime; do NOT coerce.

Extended rationale (full spec citations, separable-polish reasoning) lives in the PR description where it belongs.

Re-verification

pytest tests/test_models.py                            -> 79 passed
pytest test_additive_drift[Market,Order,Fill] -W error -> 3 passed
mypy kalshi/                                           -> Success: no issues
ruff check .                                           -> All checks passed!

Diff: +9 -22 on 3 files.

@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Follow-up Review — revisions in 67623d5

Both items from the initial review have been addressed correctly.

1. Fill import hoisted — all four inline from kalshi.models.orders import Fill (three new + one pre-existing) are gone; the module-level import now reads from kalshi.models.orders import Fill, Order. Clean.

2. Comment blocks trimmed — each block is now 2–3 lines covering only the non-obvious WHY (the list[dict] deferral, the ts/datetime distinction, the subaccount coexistence). Consistent with CLAUDE.md.

No new concerns. This PR is ready to merge.

@TexasCoding
TexasCoding merged commit 7b308fb into main May 19, 2026
5 checks passed
@TexasCoding
TexasCoding deleted the pr1/backfill-market-order-fill-v3180 branch May 19, 2026 22:36
TexasCoding added a commit that referenced this pull request May 20, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PR1: backfill v3.18.0 fields on Market / Order / Fill

1 participant