Address quality findings #909-#912 - #913
Merged
Merged
Conversation
Four findings from the /rhiza:quality scorecard, in one branch. #909 deptry: add api/ to DEPTRY_FOLDERS so the FastAPI service is scanned, and drop fastapi from the DEP002 ignore list now that its import is observed directly. kaleido, uvicorn and python-multipart stay ignored — none of the three is ever imported (image-export backend, CLI-invoked server, and a Starlette-internal parser) — but each now carries a comment saying why. #910 property tests: eight new invariants over the metric surface, chosen to cover what snapshots cannot — ordering (worst <= best, CVaR <= VaR, avg_drawdown >= max_drawdown), bounded ratios (exposure, monthly_win_rate), the self-capture identity, agreement between summary() and the metric methods it tabulates, and the truncate/smoothed_holding no-ops. #911 complexity: truncate() drops from CC 10 to 2 by extracting the date-mask and row-bounds helpers, with _rebuild() and _numeric_assets removing the three-way duplication across the transforms. _stats/_reporting.py (592 lines) splits into _reporting/_capture/_summary; _plots/_portfolio.py (588) becomes a package of _nav/_rolling/_diagnostics mixins, mirroring _plots/_data/. Also brings yearly_returns and rolling_beta down from CC 10. Extracting _beta_assets exposed a line coverage had never reached: the fallback side of a conditional *expression* is invisible to branch coverage, so the 100% gate had been passing over it. Covered by a new test rather than folded back into a ternary. #912 stability docs: the pre-1.0 policy was present but buried below the guarantee table and thin on specifics. Adds an up-front status callout and a "Before v1.0.0" section covering what is already settled, what may still move, how changes are announced, and pinning advice. Also corrects the stale private-module list (_stats.py etc. are packages now; _portfolio_data.py is gone). Coverage stays at 100% (line and branch), both import-linter contracts hold, and all eight gates pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| line={"width": 2, "color": "#1f77b4"}, | ||
| ) | ||
| ) | ||
| if baseline == baseline: # only add when baseline is finite (NaN != NaN) |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR addresses /rhiza:quality scorecard findings by expanding deptry’s scan scope to include the optional FastAPI service, strengthening correctness guarantees with additional Hypothesis property tests, and reducing complexity/size by refactoring stats/plots modules into smaller, focused units while preserving public entry points.
Changes:
- Include
api/in deptry scanning and refine DEP002 ignores with explanatory comments. - Add property-based tests for key metric invariants and for refactored behaviors (summary table contract, capture identity, transform no-ops, rolling beta fallback).
- Split large
_stats/_reporting.pyinto_reporting.py+_capture.py+_summary.py, and split_plots/_portfolio.pyinto a package with mixin modules.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_jquantstats/test_properties.py | Adds Hypothesis property tests for ordering, bounds, self-capture, summary consistency, and transform identities. |
| tests/test_jquantstats/test_migration/test_plot.py | Adds a regression test for rolling_beta asset fallback via DataLike without a returns attribute. |
| src/jquantstats/_stats/_summary.py | Introduces a dedicated mixin for summary() and annual_breakdown() extracted from reporting. |
| src/jquantstats/_stats/_stats.py | Wires new _CaptureStatsMixin and _SummaryStatsMixin into the composed Stats type. |
| src/jquantstats/_stats/_reporting.py | Narrows reporting mixin to temporal reporting metrics after extraction of capture/summary. |
| src/jquantstats/_stats/_capture.py | Introduces capture ratio mixin with shared implementation for up/down capture. |
| src/jquantstats/_portfolio_transform.py | Reduces duplication/complexity by centralizing rebuild logic and truncation helpers. |
| src/jquantstats/_plots/_portfolio/_core.py | New PortfolioPlots facade composing plot mixins (package-based portfolio plots). |
| src/jquantstats/_plots/_portfolio/init.py | Exports PortfolioPlots from the new _plots/_portfolio/ package. |
| src/jquantstats/_plots/_portfolio/_nav.py | NAV-focused plots split into a mixin module. |
| src/jquantstats/_plots/_portfolio/_rolling.py | Rolling/annual risk plots split into a mixin module and shared trace builder. |
| src/jquantstats/_plots/_portfolio/_diagnostics.py | Diagnostics plots split into a mixin module. |
| src/jquantstats/_plots/_portfolio.py | Removes the old monolithic module in favor of the new package layout. |
| src/jquantstats/_plots/_data/_styling.py | Adds _yearly_bar_colors to capture chart-specific color semantics. |
| src/jquantstats/_plots/_data/_rolling.py | Extracts reusable rolling-beta expression and assets selection helper. |
| src/jquantstats/_plots/_data/_periodic.py | Extracts shared aggregation expressions and uses yearly-specific bar coloring. |
| pyproject.toml | Updates deptry ignores: removes fastapi from DEP002 and documents remaining ignores. |
| Makefile | Appends api/ to DEPTRY_FOLDERS so deptry observes [web] imports. |
| docs/STABILITY.md | Adds explicit pre-1.0 stability policy and updates private-module list. |
Suppressed comments (1)
src/jquantstats/_plots/_portfolio/_nav.py:216
- Raising a bare
TypeErrorhere provides no actionable error message to users. Including the expected shape (list of non-negative ints) and the received value makes debugging much easier.
if windows is None:
windows = [0, 1, 2, 3, 4]
if not isinstance(windows, list) or not all(isinstance(x, int) and x >= 0 for x in windows):
raise TypeError
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+99
to
+112
| def annual_breakdown(self) -> pl.DataFrame: | ||
| """Summary statistics broken down by calendar year. | ||
|
|
||
| Groups the data by calendar year using the date index, computes a | ||
| full `summary` for each year, and stacks the results with an | ||
| additional ``year`` column. | ||
|
|
||
| Returns: | ||
| pl.DataFrame: Columns ``year``, ``metric``, one per asset, sorted | ||
| by ``year``. | ||
|
|
||
| Raises: | ||
| ValueError: If the data has no date index. | ||
| """ |
Comment on lines
+39
to
+42
| """ | ||
| if not isinstance(start, int) or not isinstance(end, int): | ||
| raise TypeError | ||
| if start > end: |
Comment on lines
+170
to
+174
| if lags is None: | ||
| lags = [0, 1, 2, 3, 4] | ||
| if not isinstance(lags, list) or not all(isinstance(x, int) for x in lags): | ||
| raise TypeError | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses the four findings from the
/rhiza:qualityscorecard.Closes #909
Closes #910
Closes #911
Closes #912
#909 — deptry scans
api/api/(the[web]extra's FastAPI service) was outside the deptry scan, so itsdependencies read as unused and were silenced. Added it to
DEPTRY_FOLDERSfrom therepo-owned
Makefileand droppedfastapifromDEP002.kaleido,uvicornandpython-multipartstay on the ignore list — none of them isever imported (a Plotly export backend selected at runtime, a CLI-invoked ASGI server,
and a parser Starlette imports on our behalf) — but each now carries a comment
explaining why, so the list records facts instead of hiding gaps.
#910 — property-based tests
The suite already had 14 hypothesis properties; the gap was breadth. Eight more, chosen
for what snapshots and line coverage structurally cannot catch:
worst <= bestCVaR <= VaRavg_drawdown >= max_drawdownexposure,monthly_win_ratein [0, 1]_capture_ratiosummary()== the metric methods it tabulatestruncate()/smoothed_holding(0)are no-opsThe
summary()property surfaced a contract worth pinning explicitly: where a metricraises (an all-zero series divides by zero in
win_rate),summarysubstitutes NaN via_saferather than propagating. The test asserts that branch instead of assuming bothpaths succeed.
#911 — complexity
truncate(): CC 10 → 2. Extracted_date_range_maskand_row_slice_bounds. Whilethere,
_rebuild()and_numeric_assetsremove a three-way duplication — all threetransforms re-listed the same
from_cash_position(...)call and the same numeric-columnfilter.
_stats/_reporting.py: 592 → 331 lines. Split along the section comments already inthe file —
_capture.py(up/down capture, which also collapses two near-identicalmethods into one
_capture_ratio) and_summary.py(summary+annual_breakdown,which carries the large cross-mixin stub block because it calls across every other
mixin).
_plots/_portfolio.py: 588 lines → a package, mirroring the existing_plots/_data/layout:_nav,_rolling,_diagnosticsmixins composed by_core.The public import path is unchanged.
rolling_sharpe_plotandrolling_volatility_plotnow share
_line_per_column.Also CC 10 → below:
yearly_returnsandrolling_betain_plots/_data/.A coverage gap this uncovered
Extracting
_beta_assetsout of a conditional expression exposed a line the 100% gatehad been passing over. coverage.py applies branch tracking to statements, not to
ternary expressions — so the fallback side of
x if cond else ywas invisible. Onceit became an
if/return, it showed up as genuinely untested. Fixed with a test thatexercises the fallback through the
DataLikeprotocol, not by restoring the ternary.#912 — pre-1.0 stability policy
STABILITY.md did already have a pre-release note, but it sat below the guarantee table
and said only "no stability guarantee". Added an up-front status callout and a
Before v1.0.0section: what is already settled (the exported names and constructors),what may still move (individual metric signatures), how changes are announced, and
pinning advice. Also corrected the stale private-module list —
_stats.py,_plots.pyand
_reports.pyare packages now, and_portfolio_data.pyno longer exists.Not done
#911's acceptance criterion said no module over ~400 lines. Seven remain, at 410–468:_stats/_performance.py,_stats/_basic_core.py,portfolio.py,data.py,exceptions.py,_stats/_basic.py,_reports/_html.py. Each is a single coherentconcern (
exceptions.pyis 19 exception classes;data.pyis theDataclass), sosplitting them would trade cohesion for a line count rather than buy clarity — unlike
the two 590-line modules this PR did split, which had visible internal seams. Flagging
rather than deciding: say the word and they can follow.
Verification
All eight gates pass: fmt, typecheck (
ty+mypy --strict, 62 files), docs-coverage(100%), deptry, security, rhiza-test (39), test (1240 passed, 100% line and branch),
and both import-linter contracts. Average complexity improved 2.84 → 2.74; no function
above CC 9.
🤖 Generated with Claude Code