Skip to content

BREAKING(v3): relocate fills/fills_all from OrdersResource to PortfolioResource#376

Merged
TexasCoding merged 3 commits into
mainfrom
v3/W4-C
May 22, 2026
Merged

BREAKING(v3): relocate fills/fills_all from OrdersResource to PortfolioResource#376
TexasCoding merged 3 commits into
mainfrom
v3/W4-C

Conversation

@TexasCoding

Copy link
Copy Markdown
Owner

Summary

fills and fills_all (sync + async) now live on PortfolioResource /
AsyncPortfolioResource, alongside settlements, deposits, and
withdrawals — the rest of the /portfolio/* family. The endpoint URL
(GET /portfolio/fills), response shape (Page[Fill]), and filters
(ticker, order_id, min_ts, max_ts, cursor, subaccount) are
identical to the previous location; only the access path changes.

OrdersResource.fills / OrdersResource.fills_all (and async counterparts)
remain in v3.0.0 as @typing_extensions.deprecated thin forwarders that
delegate to the same /portfolio/fills primitive and emit a
DeprecationWarning per call. They will be removed in a future release.

Issues closed

Migration (BREAKING in v3.0.0)

Before (v2.x):

page = client.orders.fills(ticker="KXPRES-24-DJT")
for f in client.orders.fills_all(min_ts=1_700_000_000):
    ...

After (v3.0.0):

page = client.portfolio.fills(ticker="KXPRES-24-DJT")
for f in client.portfolio.fills_all(min_ts=1_700_000_000):
    ...

The old call sites still compile and run in v3.0.0, but each call emits a
single DeprecationWarning pointing at the new location.

Behavioral changes

  • client.orders.fills(...) / client.orders.fills_all(...) now emit
    DeprecationWarning (one per call). Existing wire behavior is unchanged.
  • client.portfolio.fills(...) / client.portfolio.fills_all(...) are new
    public methods with identical signatures and identical request shape.

Tests

  • tests/test_portfolio.pyTestPortfolioFills, TestPortfolioFillsAll,
    TestAsyncPortfolioFills, TestAsyncPortfolioFillsAll cover the new
    location (happy path, cursor/filter parity, sync + async iterator).
  • tests/test_orders.py::TestIssue351OrdersFillsDeprecated — asserts the
    old forwarders still resolve and emit exactly one DeprecationWarning
    per call.
  • tests/test_async_orders.py::TestIssue351AsyncOrdersFillsDeprecated
    same for the async forwarders.
  • Pre-existing TestOrdersFills / TestAsyncOrdersFills test classes are
    preserved unchanged; they still cover the forwarder's wire behavior and
    produce informational DeprecationWarnings in test output.

Source

Round-3 independent audit closure plan, wave W4 (v3.0.0 breaking renames).

`fills` and `fills_all` (sync + async) move from `OrdersResource` to
`PortfolioResource`, grouping them with the rest of the `/portfolio/*`
family (`settlements`, `deposits`, `withdrawals`). The endpoint URL
`GET /portfolio/fills`, response shape `Page[Fill]`, and filters
(`ticker`, `order_id`, `min_ts`, `max_ts`, `cursor`, `subaccount`)
are unchanged — only the access path on the client moves.

`OrdersResource.fills` / `OrdersResource.fills_all` (and async equivalents)
remain in v3.0.0 as `@typing_extensions.deprecated` thin forwarders that
delegate to the same `/portfolio/fills` primitive and emit a single
`DeprecationWarning` per call. They will be removed in a future release.

Tests cover the new location (happy path + cursor/filter parity, sync and
async) and the deprecation behavior of the old location (single warning per
call, behavior preserved). Docs in `docs/resources/portfolio.md` add a
`Fills` section; `docs/resources/orders.md` keeps a one-line forwarder
note pointing at the new location.

Closes #351
@TexasCoding TexasCoding added polish Code-quality and DX improvements; non-functional breaking Backwards-incompatible change labels May 22, 2026
@claude

claude Bot commented May 22, 2026

Copy link
Copy Markdown

Code Review — PR #376: Relocate fills/fills_all to PortfolioResource

Overall: The migration strategy is solid — @deprecated forwarders, clean new implementations, and good test coverage. However there are two correctness gaps that will fail CI and one structural issue worth fixing before merge.


Critical: Missing _contract_support.py updates

The new PortfolioResource.fills and PortfolioResource.fills_all methods are not registered in METHOD_ENDPOINT_MAP in tests/_contract_support.py. CLAUDE.md is explicit: "Drift tests hard-fail. Adding a new kwarg the spec doesn't have, or missing one the spec has, reds CI."

By comparison, every other portfolio method has entries:

# settlements, deposits, withdrawals are all registered; fills is not
sdk_method="kalshi.resources.portfolio.PortfolioResource.settlements",
sdk_method="kalshi.resources.portfolio.PortfolioResource.settlements_all",

The PR needs to add:

  1. METHOD_ENDPOINT_MAP entries for PortfolioResource.fills, PortfolioResource.fills_all, and their async counterparts.
  2. cursor exclusions for fills_all / AsyncPortfolioResource.fills_all — same pattern as settlements_all, deposits_all, etc.:
    ("kalshi.resources.portfolio.PortfolioResource.fills_all", "cursor"): Exclusion(reason="..."),
    ("kalshi.resources.portfolio.AsyncPortfolioResource.fills_all", "cursor"): Exclusion(reason="..."),
  3. PortfolioResource.fills_all and AsyncPortfolioResource.fills_all added to _MAX_PAGES_FQNS alongside settlements_all, deposits_all, etc.

Structural: Duplicated _fills_params function

_fills_params is now defined identically in both kalshi/resources/orders.py and kalshi/resources/portfolio.py. The bodies are byte-for-byte the same (keyword args, _validate_limit(limit, hi=1000), and _params(...)). This is the exact duplication the _base_client.py shared helpers exist to prevent.

Options (ranked by invasiveness):

  • Simplest (recommended): Import _fills_params from orders.py into portfolio.py. The dependency direction is a bit odd but avoids a shared-module change.
  • Cleaner: Move _fills_params to kalshi/resources/_base.py alongside _params and _validate_limit, then import from both.

Either way, one copy should go.


Minor: Unnecessary inline # NOTE: comments in orders.py forwarders

The deprecated forwarders in orders.py gained # NOTE: still hits /portfolio/fills directly comments. Per CLAUDE.md: "Default to writing no comments. Only add one when the WHY is non-obvious." The docstring directly above each method already says exactly this. The inline comments don't add information and should be removed.


What's done well

  • @deprecated from typing_extensions — correct tool for static-analysis-visible deprecation, triggers IDE warnings and mypy.
  • Forwarder strategy — old call sites still compile and run in v3.0.0 with a single DeprecationWarning per call. Clean migration path.
  • _fills_params in portfolio.py — correctly follows the _settlements_params pattern, including the hi=1000 limit cap.
  • AsyncPortfolioResource.fills_all as def not async def — correct; _list_all returns an AsyncIterator directly, matching the established pattern on AsyncOrdersResource.
  • Tests: TestPortfolioFills (wire params parity), TestPortfolioFillsAll (two-page pagination), TestIssue351*Deprecated (exactly-one-warning assertion), sync + async coverage. This is thorough.
  • Docs: Both orders.md and portfolio.md updated in the same PR, and the migration note is clear and actionable.

Summary

Status
_contract_support.py METHOD_ENDPOINT_MAP entries ❌ Missing
cursor / max_pages exclusions for new fills_all ❌ Missing
Duplicated _fills_params ⚠️ Should fix
Inline NOTE comments ⚠️ Remove
Tests ✅ Good coverage
Deprecation strategy ✅ Correct
Docs ✅ Updated

The first gap is a CI-hard-fail by design (the repo's own drift tests will catch it). The second is a structural debt item. Both are straightforward fixes.

@TexasCoding

Copy link
Copy Markdown
Owner Author

Round-2 review addressed in c490866: registered new PortfolioResource.fills/fills_all (sync + async) in METHOD_ENDPOINT_MAP and _MAX_PAGES_FQNS mirroring the existing OrdersResource entries. Old OrdersResource.fills/fills_all entries retained for the deprecated forwarders.

@claude

claude Bot commented May 22, 2026

Copy link
Copy Markdown

Code Review — PR #376: Relocate fills/fills_all to PortfolioResource

Overall, this is a well-structured breaking change with a solid migration path. The deprecated forwarders, drift-test updates, and docs update are all handled correctly. A few things to address before merge:


Correctness / Bugs

_fills_params is duplicated verbatim between orders.py and portfolio.py

Both files define an identical _fills_params helper (same signature, same _validate_limit(limit, hi=1000), same _params(...) body). The PR correctly avoids importing it across resource files (which would be a weird dep direction), but the duplication means a future limit-change only gets fixed in one place. Suggest either:

  • Moving it to kalshi/resources/_base.py alongside _params/_validate_limit (the natural home), or
  • Leaving it duplicated with a comment noting they must stay in sync.

The current state is a silent footgun.

Auth-required tests are missing for the new portfolio.fills and portfolio.fills_all

Every other PortfolioResource method has a corresponding requires_auth guard test (e.g. test_positions_all_requires_auth on line 1131). The new TestPortfolioFills / TestPortfolioFillsAll / TestAsyncPortfolioFills / TestAsyncPortfolioFillsAll classes don't include one. Since _require_auth() is called in both methods, a test to confirm AuthRequiredError is raised without credentials would close that gap and match the existing pattern.


Style / Conventions (CLAUDE.md)

# NOTE: comments in deprecated forwarders are redundant

Each deprecated fills / fills_all body in orders.py now has:

# NOTE: still hits /portfolio/fills directly — the canonical
# implementation now lives on PortfolioResource (issue #351).

The docstring and @deprecated(...) message already communicate this. Per CLAUDE.md: "Only add [a comment] when the WHY is non-obvious." Here it's already explicit. These 6 comment blocks can be dropped.

Multi-line docstrings on methods slated for removal

The deprecated forwarders now carry verbose Sphinx-format docstrings. Per CLAUDE.md, docstrings that explain what (Use :meth:PortfolioResource.fills instead.) rather than why are unnecessary — especially for code that will be deleted in one release. The @deprecated(...) message on the decorator already surfaces in IDEs. The docstrings can be removed or collapsed to the single line the original had.


Contract Drift Tests

Cursor exclusion for AsyncPortfolioResource.fills_all is absent — intentionally?

The diff adds:

("kalshi.resources.portfolio.PortfolioResource.fills_all", "cursor"): Exclusion(...)

but no matching entry for AsyncPortfolioResource.fills_all. Looking at the existing map, async resources aren't in METHOD_ENDPOINT_MAP at all, so this is consistent with the rest of the codebase — just worth confirming it's intentional and not an oversight.


Tests — What's Good

  • TestIssue351OrdersFillsDeprecated / TestIssue351AsyncOrdersFillsDeprecated correctly verify exactly one DeprecationWarning per call and match the deprecation message with r"OrdersResource\.fills".
  • test_fills_cursor_and_filter_parity inspects raw wire params — this is the right level of rigor for a location move.
  • Pagination is covered with a two-page mock in TestPortfolioFillsAll / TestAsyncPortfolioFillsAll.

Summary

Issue Severity
_fills_params duplicated in orders.py and portfolio.py Medium — silent maintenance trap
Auth-required tests missing for portfolio.fills / portfolio.fills_all Low — coverage gap, existing forwarder tests cover the wire path
Redundant # NOTE: comments in deprecated forwarders Nit (CLAUDE.md)
Verbose docstrings on to-be-deleted methods Nit (CLAUDE.md)
Missing async cursor exclusion Low — likely intentional, worth a comment in the code

@TexasCoding

Copy link
Copy Markdown
Owner Author

Round-2 review addressed in 0ff01cd: deduped _fills_params to kalshi/resources/_base.py; added 4 auth-required tests for new PortfolioResource fills variants; trimmed 6 redundant NOTE comments and collapsed forwarder docstrings to single-line. Deferred: missing async cursor exclusion note (consistent with rest of codebase, no async entries in METHOD_ENDPOINT_MAP).

@claude

claude Bot commented May 22, 2026

Copy link
Copy Markdown

Code Review — PR #376: BREAKING(v3): relocate fills/fills_all to PortfolioResource

Overview

Clean, well-scoped relocation. The motivation is sound — GET /portfolio/fills belongs with the rest of the /portfolio/* family. The deprecation shim approach (thin forwarders with @typing_extensions.deprecated) is the right migration pattern. The contract map, exclusion lists, and docs are all updated in lockstep.


Issues

1. Multi-paragraph docstrings violate CLAUDE.md style ⚠️

kalshi/resources/portfolio.py — new fills() and fills_all() methods (sync and async) have multi-paragraph docstrings:

"""List trade fills (``GET /portfolio/fills``).

Moved from :class:`OrdersResource` in v3.0.0 (issue #351) to group
with the rest of the ``/portfolio/*`` family (``settlements``,
``deposits``, ``withdrawals``).
"""

CLAUDE.md is explicit: "Never write multi-paragraph docstrings or multi-line comment blocks — one short line max." Collapse to a single line, e.g.:

"""List trade fills (``GET /portfolio/fills``). Moved from OrdersResource in v3.0.0 (#351)."""

Same applies to the async variants.


2. Stray blank-line removal in portfolio.py — surgical violation

The diff removes a blank line between _settlements_params and class PortfolioResource. This line was not introduced by this PR and its removal doesn't trace to the stated goal. Per CLAUDE.md: "Don't 'improve' adjacent code, comments, or formatting."


3. Missing max_pages enforcement test for fills_all ⚠️

TestPortfolioFillsAll and TestAsyncPortfolioFillsAll cover the happy path and auth guard but skip max_pages cap enforcement. Compare TestPortfolioDeposits.test_all_max_pages_caps, which asserts that max_pages=1 stops after one page. The same gap exists in TestAsyncPortfolioFillsAll. All other *_all methods on PortfolioResource have this test.

def test_fills_all_max_pages_caps(self, portfolio: PortfolioResource) -> None:
    respx.get(...).mock(side_effect=[
        httpx.Response(200, json={"fills": [fill_dict(trade_id="a")], "cursor": "p2"}),
        # second page should never be fetched
    ])
    ids = [f.trade_id for f in portfolio.fills_all(max_pages=1)]
    assert ids == ["a"]

4. Missing filter-forwarding test for fills_all

TestPortfolioFills.test_fills_cursor_and_filter_parity verifies every query param reaches the wire for the single-page method. There's no equivalent for fills_all. Since fills_all builds params through _fills_params and then _list_all (which injects cursor internally), a test confirming ticker, order_id, min_ts, etc., are present in the first paginated request would close this gap — the same as TestPortfolioSettlements.test_settlements_all_with_all_new_filters.


5. Double blank line before TestIssue351OrdersFillsDeprecated in test_orders.py

Two consecutive blank lines appear before the # ── Issue #351 … comment block. PEP 8 / project style is one blank line between top-level test classes.


What's correct

  • _fills_params correctly promoted to _base.py (single source of truth for both resources).
  • @typing_extensions.deprecated correctly applied to all four forwarders (sync fills, sync fills_all, async fills, async fills_all).
  • AsyncPortfolioResource.fills_all correctly returns AsyncIterator[Fill] as a non-async def — consistent with every other *_all on async resources.
  • Contract map entries (sync PortfolioResource only, matching existing convention — async methods are not in METHOD_ENDPOINT_MAP).
  • cursor exclusion added only for PortfolioResource.fills_all (sync), which is the only method actually in METHOD_ENDPOINT_MAP.
  • AsyncPortfolioResource.fills_all added to _list_all_methods exclusion lists — correct.
  • Deprecation tests assert exactly one warning per call — good.
  • Docs updated in both orders.md and portfolio.md — clear and accurate.

Summary

Straightforward approval pending: fixing the docstring lengths (#1), restoring the blank line (#2), and adding the two missing test cases (#3, #4). The double blank line (#5) is cosmetic. Nothing blocking in logic or safety.

@claude

claude Bot commented May 22, 2026

Copy link
Copy Markdown

Code Review — PR #376: Relocate fills/fills_all to PortfolioResource

Overview

Clean, well-scoped relocation of fills/fills_all (sync + async) from OrdersResource to PortfolioResource, where they semantically belong alongside settlements, deposits, and withdrawals. The deprecated forwarders, contract registrations, and docs all look solid. A few items worth flagging below.


Issues

1. Forwarders are not true delegations — they duplicate the endpoint call

OrdersResource.fills and PortfolioResource.fills both independently call self._list("/portfolio/fills", ...). They share params via _fills_params but are otherwise copies. The PR description says they "delegate to the same /portfolio/fills primitive," which is technically accurate but could mislead future contributors into thinking a change to PortfolioResource.fills propagates to the forwarder. If PortfolioResource.fills gains additional logic (auth scoping, feature flags, response post-processing), the deprecated forwarder silently diverges.

If the KalshiClient is accessible from within OrdersResource, true delegation (return self._client.portfolio.fills(...)) is preferable and removes the duplication risk. If not, this is fine as-is — just worth a note in the forwarder's docstring: """Deprecated — delegates directly to /portfolio/fills; see PortfolioResource.fills."""

2. PEP 8: blank line removed before class PortfolioResource

The diff removes the blank line between _settlements_params and class PortfolioResource:

-
 class PortfolioResource(SyncResource):

PEP 8 requires two blank lines between a module-level function and the following class. This is a one-character fix but will fail ruff if the project enforces E302.


Observations (non-blocking)

3. AsyncPortfolioResource.fills_all is correctly def, not async def — consistent with every other *_all method on async resources in this file. Not a bug; confirmed as the established pattern.

4. @deprecated placement on fills_all in AsyncOrdersResource — the decorator is on the def fills_all(...) which returns AsyncIterator. typing_extensions.deprecated wraps it as a regular function call, and the warning fires at call time (when the iterator is constructed), not at iteration time. This is the right behavior.

5. Contract map is consistentAsyncPortfolioResource.fills is intentionally absent from METHOD_ENDPOINT_MAP (only sync methods are registered there, consistent with all other async resources). ✓

6. No CHANGELOG entry — intentional per project convention (updated per release). ✓


Tests

Test coverage is thorough:

  • Happy path, filter parity, and auth guard for both fills and fills_all on both sync and async PortfolioResource. ✓
  • Exactly-one-warning assertions on the deprecated forwarder paths (TestIssue351OrdersFillsDeprecated, TestIssue351AsyncOrdersFillsDeprecated). ✓
  • Two-page pagination test for fills_all. ✓

One gap: there's no test asserting that calling portfolio.fills_all() does not emit a DeprecationWarning (to guard against accidentally decorating the new canonical location). Low-priority, but could catch a copy-paste error if the @deprecated decorator ever migrates wrongly.


Summary

Approve with the two items above addressed (especially the blank-line regression if ruff is enforced in CI). The relocation is correct, the deprecation strategy is appropriate for a breaking v3.0.0 change, and the test suite is comprehensive.

@TexasCoding
TexasCoding merged commit eab8dbc into main May 22, 2026
5 checks passed
TexasCoding added a commit that referenced this pull request May 22, 2026
…ests

Post-v3.0.0 release surfaced 5 failing integration tests (release was
merged with bypassed status checks). All five are test maintenance,
not SDK bugs:

Coverage harness:
- Expected resource set now includes v3 sub-resources RFQsResource
  and QuotesResource (21 classes, was 19).
- Register PortfolioResource.{fills, fills_all, positions_all} —
  relocated from OrdersResource in #351/#376.
- Register MarketsResource.list_all_trades (canonical v3 name).
- Register FcmResource.positions_all (added in #344).
- Register RFQsResource + QuotesResource public methods.

Integration tests for v3 surfaces:
- tests/integration/test_communications.py: add TestRFQsV3Sync,
  TestQuotesV3Sync, TestQuotesV3RealApiOnly — exercise the namespaced
  client.communications.{rfqs,quotes}.* surface end-to-end on demo.
- tests/integration/test_portfolio.py: add fills/fills_all/positions_all
  smoke tests on both sync and async.
- tests/integration/test_markets.py: add test_list_all_trades.
- tests/integration/test_fcm.py: add positions_all sync + async.

Demo-drift fixes in test_errors.py:
- test_malformed_params_returns_validation_error +
  test_validation_error_details_attribute: demo now silently accepts
  malformed cursors. Switched to status='not-a-real-status' which
  reliably triggers 400 invalid_status_filter.
- test_bad_auth_returns_auth_error: KalshiConfig(base_url=...) now
  rejects split REST/WS environments (defense-in-depth from #261).
  Switched to KalshiConfig.demo().

Verification:
  Unit:        2954 passed, 3 skipped
  Integration: 233 passed, 38 skipped (was 218 passed / 5 failed)
@TexasCoding
TexasCoding deleted the v3/W4-C branch May 22, 2026 23:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Backwards-incompatible change polish Code-quality and DX improvements; non-functional

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OrdersResource.fills / fills_all hits /portfolio/fills — belongs on PortfolioResource

1 participant