feat(models): backfill v3.18.0 fields on events / portfolio / historical / incentive_programs (PR2)#167
Conversation
…cal / incentive_programs (#160) Response-side spec drift hardening, PR2 of the stack (#157). 9 new optional fields across 5 mid-tier response models. Eliminates exactly 9 additive-drift warning bullets (the issue's stated target). Fields added ------------ `Event` (+3): fee_type_override (str), fee_multiplier_override (Decimal — type:number,format:double per spec; matches Market floor_strike/cap_strike precedent), exchange_index (int) `EventMetadata` (+2): competition (str), competition_scope (str) `Settlement` (+1): value (int — integer cents per spec, NOT DollarDecimal) `Trade` (+2): taker_outcome_side (SideLiteral), taker_book_side (BookSideLiteral) — mirrors Order.outcome_side / book_side from PR1, superseding the deprecated `taker_side` `IncentiveProgram` (+1): incentive_description (str) Design decisions ---------------- - `fee_multiplier_override` typed as `Decimal | None`, not `DollarDecimal`. Spec says `type: number, format: double, nullable: true` — same shape as Market.floor_strike / cap_strike (already Decimal | None). DollarDecimal is for `type: string` dollar-suffix wire fields; this field is a numeric multiplier ratio, not a dollar amount. - `Settlement.value` stays `int | None`, NOT DollarDecimal. Spec: "Payout of a single yes contract in cents." Plain integer cents; auto- coercing to a decimal would break the contract. Test includes a regression guard asserting `isinstance(s.value, int)` and not bool. - `Trade.taker_outcome_side` / `taker_book_side` reuse the existing `SideLiteral` / `BookSideLiteral` aliases from kalshi.models.orders. This is the first cross-module import in kalshi/models/ for the new fields — no circular import (orders.py doesn't import historical.py). Import order in historical.py sorted alphabetically per ruff isort. - All new fields appended at the end of each model with a `# v3.18.0 backfill (#160)` marker. Append-only, matches PR1 pattern. Tests ----- 5 new test classes in `tests/test_models.py` (10 tests). Placed alongside the PR1 test classes (`TestMarketV3180Fields` etc.) rather than per- resource files, matching PR1 precedent that the reviewer accepted: - TestEventV3180Fields (2 tests) - TestEventMetadataV3180Fields (2 tests) - TestSettlementV3180Fields (2 tests — includes int-not-decimal regression) - TestTradeV3180Fields (2 tests) - TestIncentiveProgramV3180Fields (2 tests) Also hoisted Event / EventMetadata / Trade / IncentiveProgram / Settlement to module-level imports in `test_models.py` to avoid the inline-import pattern PR1's reviewer flagged. Pre-existing inline imports elsewhere in the file left untouched (out of scope). Verification ------------ uv run pytest tests/test_contracts.py::TestSpecDrift::test_additive_drift[Event,EventMetadata,Settlement,Trade,IncentiveProgram] -W error -> 5 passed (was: 5 failed) uv run pytest tests/ --ignore=tests/integration -q -> 1965 passed, 50 warnings (baseline 1955 + 10 new; warnings 54 -> 50) uv run mypy kalshi/ -> Success: no issues uv run ruff check . -> All checks passed! Warning delta breakdown: 9 additive bullets eliminated (Event:3, EventMetadata:2, Settlement:1, Trade:2, IncentiveProgram:1). One new required-but-optional warning appears for `IncentiveProgram.incentive_description` (spec marks it required, but project policy is `| None = None` for response fields). Net test count: 5 fewer additive-drift failures - 1 new required-drift failure = 4 fewer. Required-but-optional is explicitly out of scope per the issue. Closes #160.
Code Review — PR #167: v3.18.0 Backfill (Events / Portfolio / Historical / IncentivePrograms)Overall this is a clean, minimal, well-documented change. The design decisions are well-reasoned and the implementation follows established patterns. A few observations below. What the PR DoesAdds 9 optional fields across 5 response models to close spec drift against OpenAPI v3.18.0 (issue #160). No resource methods, request models, or contract maps are changed — purely additive response-side backfill. Correctness ✅All typing choices hold up:
Minor Issues1. Multi-line comment blocks in production code (CLAUDE.md: "one short line max")
# v3.18.0 backfill (#160). fee_multiplier_override is `type: number,
# format: double` per spec — Decimal (matches Market.floor_strike /
# cap_strike precedent for the same wire shape).CLAUDE.md says "Never write multi-paragraph docstrings or multi-line comment blocks — one short line max." The comment in
The same issue exists in PR1/PR2's companion blocks in 2. Test input for # test_parses_new_fields in TestEventV3180Fields
Event.model_validate({..., "fee_multiplier_override": "1.25", ...})The spec's "fee_multiplier_override": 1.25 # float — matches actual JSON wire formatThis is a minor suggestion, not a blocking concern. Test Coverage ✅10 new tests across 5 classes; every new field has both a "parses correctly" and "defaults to None" case. That's the right minimum. No gaps visible. No Action Needed (Already Handled)
SummaryThe implementation is correct and consistent with existing patterns. The two items above are both non-blocking:
Happy to approve as-is or after either cleanup — your call. |
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 #160. PR2 of the response-side spec-drift hardening stack (#157). Builds on the EXCLUSIONS infra from #165 and the patterns established in #166. Five more
test_additive_driftcases pass; three models (#PR3, #PR4) still ahead before the warn→fail flip (#PR5).What this changes
9 new optional fields across 5 mid-tier response models, matching the v3.18.0 OpenAPI spec.
kalshi/models/events.pyEvent(+3):fee_type_overridestr | Nonefee_multiplier_overrideDecimal | Noneexchange_indexint | NoneEventMetadata(+2):competitionstr | Nonecompetition_scopestr | Nonekalshi/models/portfolio.pySettlement(+1):valueint | Nonekalshi/models/historical.pyTrade(+2):taker_outcome_sideSideLiteral | Nonetaker_book_sideBookSideLiteral | Nonekalshi/models/incentive_programs.pyIncentiveProgram(+1):incentive_descriptionstr | NoneDesign decisions
Event.fee_multiplier_overrideisDecimal | None, notDollarDecimal. Spec declarestype: number, format: double, nullable: true— the same shape asMarket.floor_strikeandMarket.cap_strike(already typed asDecimal | None).DollarDecimalis specifically fortype: stringdollar-suffix wire fields (e.g.yes_bid_dollars); this field is a unitless numeric multiplier, not a dollar amount.Settlement.valuestaysint | None, NOTDollarDecimal. Spec description: "Payout of a single yes contract in cents." Plain integer cents — auto-coercing to a decimal would change the contract. Test class includes atest_value_is_int_not_decimalregression guard withisinstance(s.value, int)+not isinstance(s.value, bool).Trade.taker_outcome_side/taker_book_sidereuse existingSideLiteral/BookSideLiteralaliases fromkalshi.models.orders. First cross-module import inkalshi/models/for backfill fields; no circular import (orders.pydoesn't reach back intohistorical.py). Import order inhistorical.pysorted alphabetically per ruff isort.IncentiveProgram.incentive_descriptionisstr | None = Nonedespite spec marking it required. Per the project's pervasive permissive-response-model policy; matches how every existingIncentiveProgramfield is typed. Issue defers required-but-optional policy.# v3.18.0 backfill (#160)marker. Append-only, matches PR1 pattern.No changes to request-body models or resource methods. No changes to the contract map.
Tests
5 new test classes in
tests/test_models.py(10 tests). I placed them alongside the PR1 test classes for grouping consistency, rather than the per-resource files the issue suggested — same precedent the reviewer accepted in PR1 (which also went intotest_models.py).TestEventV3180FieldsTestEventMetadataV3180FieldsTestSettlementV3180FieldsTestTradeV3180FieldsTestIncentiveProgramV3180FieldsAlso hoisted
Event,EventMetadata,Trade,IncentiveProgram,Settlementto module-level imports intest_models.py— avoiding the inline-import pattern PR1's reviewer flagged. Pre-existing inline imports elsewhere in the file left untouched (out of scope).Verification
Note on the warning delta
The issue states this PR "removes 9 of the remaining additive-drift warnings." That's measured in warning bullets: Event:3 + EventMetadata:2 + Settlement:1 + Trade:2 + IncentiveProgram:1 = 9 — matches exactly.
Net test failure count went from 54 to 50 (−4), not −5, because adding
IncentiveProgram.incentive_descriptionasstr | Noneintroduces one newtest_required_drift[IncentiveProgram]warning (spec marks the field required; project policy is permissive). This is consistent with every other field in the SDK; the issue explicitly defers required-but-optional drift as a separate ≈204-entry decision.Part of the response-side spec drift hardening stack (#157).