Skip to content

feat(models): backfill v3.18.0 fields on events / portfolio / historical / incentive_programs (PR2)#167

Merged
TexasCoding merged 1 commit into
mainfrom
pr2/backfill-events-portfolio-historical-incentive-v3180
May 19, 2026
Merged

feat(models): backfill v3.18.0 fields on events / portfolio / historical / incentive_programs (PR2)#167
TexasCoding merged 1 commit into
mainfrom
pr2/backfill-events-portfolio-historical-incentive-v3180

Conversation

@TexasCoding

Copy link
Copy Markdown
Owner

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_drift cases 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.py

Event (+3):

Field SDK type
fee_type_override str | None
fee_multiplier_override Decimal | None
exchange_index int | None

EventMetadata (+2):

Field SDK type
competition str | None
competition_scope str | None

kalshi/models/portfolio.py

Settlement (+1):

Field SDK type
value int | None

kalshi/models/historical.py

Trade (+2):

Field SDK type
taker_outcome_side SideLiteral | None
taker_book_side BookSideLiteral | None

kalshi/models/incentive_programs.py

IncentiveProgram (+1):

Field SDK type
incentive_description str | None

Design decisions

  • Event.fee_multiplier_override is Decimal | None, not DollarDecimal. Spec declares type: number, format: double, nullable: true — the same shape as Market.floor_strike and Market.cap_strike (already typed as Decimal | None). DollarDecimal is specifically for type: string dollar-suffix wire fields (e.g. yes_bid_dollars); this field is a unitless numeric multiplier, not a dollar amount.
  • Settlement.value stays int | None, NOT DollarDecimal. 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 a test_value_is_int_not_decimal regression guard with isinstance(s.value, int) + not isinstance(s.value, bool).
  • Trade.taker_outcome_side / taker_book_side reuse existing SideLiteral / BookSideLiteral aliases from kalshi.models.orders. First cross-module import in kalshi/models/ for backfill fields; no circular import (orders.py doesn't reach back into historical.py). Import order in historical.py sorted alphabetically per ruff isort.
  • IncentiveProgram.incentive_description is str | None = None despite spec marking it required. Per the project's pervasive permissive-response-model policy; matches how every existing IncentiveProgram field is typed. Issue defers required-but-optional policy.
  • All new fields appended at the end of each model with a # 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 into test_models.py).

Class Tests
TestEventV3180Fields parses fields, defaults to None
TestEventMetadataV3180Fields parses fields, defaults to None
TestSettlementV3180Fields value-is-int-not-decimal regression, defaults to None
TestTradeV3180Fields parses fields, defaults to None
TestIncentiveProgramV3180Fields parses field, defaults to None

Also hoisted Event, EventMetadata, Trade, IncentiveProgram, Settlement to module-level imports in test_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

$ uv run pytest tests/test_contracts.py::TestSpecDrift::test_additive_drift[Event,EventMetadata,Settlement,Trade,IncentiveProgram] -W error
5 passed                                          # was: 5 failed before this PR

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

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

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_description as str | None introduces one new test_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).

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

claude Bot commented May 19, 2026

Copy link
Copy Markdown

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 Does

Adds 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:

  • fee_multiplier_override: Decimal | None — correct. Spec says type: number, format: double; Decimal matches the Market.floor_strike / cap_strike precedent for the same wire shape.
  • Settlement.value: int | None — correct. Spec description ("payout in cents") confirms plain integer; the isinstance(s.value, bool) regression guard is a nice touch since bool is a subclass of int.
  • SideLiteral / BookSideLiteral cross-import — no circular import risk; orders.py has no dependency on historical.py.
  • IncentiveProgram.incentive_description as optional despite spec marking it required — consistent with the project's permissive-response-model policy; correctly documented.

Minor Issues

1. Multi-line comment blocks in production code (CLAUDE.md: "one short line max")

kalshi/models/events.py has a 3-line comment block:

# 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 portfolio.py and historical.py also warrant trimming:

  • portfolio.py: the value comment explains the int choice inline — fine as a single-line if condensed
  • historical.py: similar 2-line block

The same issue exists in PR1/PR2's companion blocks in orders.py (pre-existing), but worth keeping consistent going forward.

2. Test input for fee_multiplier_override uses a string, but the wire format is a JSON float

# test_parses_new_fields in TestEventV3180Fields
Event.model_validate({..., "fee_multiplier_override": "1.25", ...})

The spec's type: number, format: double means the API returns a JSON number, which Python parses as float, not str. Pydantic v2 coerces both, so the test passes, but it's testing a less realistic input path. Consider:

"fee_multiplier_override": 1.25  # float — matches actual JSON wire format

This 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)

  • _contract_map.py unchanged — appropriate since no new resource endpoints were added.
  • __init__.py exports unchanged — the models already existed; only fields were added.
  • The net test_required_drift delta (+1 for incentive_description) is explained and expected.

Summary

The implementation is correct and consistent with existing patterns. The two items above are both non-blocking:

  1. Multi-line comment trimming (style)
  2. Test input realism for fee_multiplier_override (minor)

Happy to approve as-is or after either cleanup — your call.

@TexasCoding
TexasCoding merged commit 0cb0d14 into main May 19, 2026
5 checks passed
@TexasCoding
TexasCoding deleted the pr2/backfill-events-portfolio-historical-incentive-v3180 branch May 19, 2026 22:58
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.

PR2: backfill v3.18.0 fields on events / portfolio / historical / incentive_programs

1 participant