feat: backtest-honesty module (PBO/CSCV, MinTRL/MinBTL, overfitting verdict)#27
Merged
Merged
Conversation
…verdict New overfitting.py flags when a backtest is likely a fluke: - probability_of_backtest_overfitting: PBO via Combinatorially Symmetric CV. - min_track_record_length / min_backtest_length: closed-form MinTRL / MinBTL. - assess_overfitting: one-call verdict (robust / caution / likely_overfit / inconclusive) bundling PBO + deflated Sharpe + MinTRL. - overfitting_section: embeddable HTML report section. Consume-only, offline, no new dependencies; reuses performance_stats' moment and deflated-Sharpe machinery. Exported from the package; CHANGELOG + README updated. Built test-first (22 unit tests: known-answer PBO on noise/dominant/ overfit constructions, MinTRL/MinBTL monotonicity, verdict wiring, section render). Refs: Bailey & Lopez de Prado, Deflated Sharpe Ratio / Probability of Backtest Overfitting. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011a6EibcjDDv3Ff1Fb5esS1
Reviewer's GuideIntroduces a new backtest-honesty / overfitting diagnostics module that computes PBO via CSCV, minimum track record/backtest lengths, a combined overfitting verdict, and an embeddable HTML report section, wires it into the public API, and documents the new capability. Sequence diagram for assess_overfitting verdict computationsequenceDiagram
participant Client
participant quant_reporter as quant_reporter
participant overfitting as overfitting_module
participant performance_stats
Client->>quant_reporter: assess_overfitting(returns_matrix, returns, n_trials)
quant_reporter->>overfitting: assess_overfitting(returns_matrix, returns, n_trials)
alt returns_matrix provided
overfitting->>overfitting: _as_matrix(returns_matrix)
overfitting->>overfitting: probability_of_backtest_overfitting(df, n_splits)
overfitting->>overfitting: _best_series(df, periods_per_year)
end
alt returns provided
overfitting->>performance_stats: deflated_sharpe_ratio(returns, n_trials, periods_per_year)
overfitting->>overfitting: min_track_record_length(returns, periods_per_year)
end
overfitting-->>Client: OverfittingReport
Client->>quant_reporter: overfitting_section(report)
quant_reporter->>overfitting: overfitting_section(report)
overfitting-->>Client: section dict (HTML banner)
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- In
probability_of_backtest_overfitting,all_combos = list(combinations(...))can become very large before being subsampled; consider short-circuiting based on an upper bound onn_splitsor generating a random subset of combinations without materializing the full list to avoid potential memory blowups. _as_matrixcurrently drops any row with a NaN in any configuration (dropna(how="any")), which can throw away a lot of data for long backtests; consider allowing per-series NaNs (e.g., forward-filling or dropping per-column) or at least making this behavior explicit/optional so users don’t unexpectedly lose observations.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `probability_of_backtest_overfitting`, `all_combos = list(combinations(...))` can become very large before being subsampled; consider short-circuiting based on an upper bound on `n_splits` or generating a random subset of combinations without materializing the full list to avoid potential memory blowups.
- `_as_matrix` currently drops any row with a NaN in any configuration (`dropna(how="any")`), which can throw away a lot of data for long backtests; consider allowing per-series NaNs (e.g., forward-filling or dropping per-column) or at least making this behavior explicit/optional so users don’t unexpectedly lose observations.
## Individual Comments
### Comment 1
<location path="src/quant_reporter/overfitting.py" line_range="60-69" />
<code_context>
+ return 1.0 + max(var_factor, 0.0) * (z / (sr - sr_star_pp)) ** 2
+
+
+def min_backtest_length(n_trials, sr_target_annual=1.0, periods_per_year=252):
+ """Minimum Backtest Length (in years) before ``n_trials`` fake ``sr_target_annual``.
+
+ The expected maximum Sharpe of ``n_trials`` skill-less i.i.d. strategies grows
+ like ``Z_N / sqrt(T)``; solving for the horizon at which that expected fluke
+ stays below the annualized target gives ``MinBTL = (Z_N / sr_target_annual)**2``
+ years, where ``Z_N`` is the expected maximum of ``n_trials`` standard normals.
+ One trial cannot be an overfit selection, so returns 0.0. Returns ``nan`` for a
+ non-positive target.
+ """
+ if sr_target_annual <= 0:
+ return float("nan")
+ if n_trials is None or n_trials <= 1:
+ return 0.0
+ z_n = _expected_max_sr(n_trials, 1.0) # expected max of n_trials standard normals
+ return float((z_n / sr_target_annual) ** 2)
+
</code_context>
<issue_to_address>
**nitpick:** The periods_per_year parameter is unused in min_backtest_length and may be confusing.
The function always computes MinBTL in years using a standard-normal assumption and ignores the periods_per_year argument, so callers cannot actually change the time scale. Please either incorporate periods_per_year into the calculation or remove it from the signature to avoid a misleading API.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Address Sourcery nitpick on #27: MinBTL is intrinsically in years (Z_N is unitless), so periods_per_year was a dead, misleading parameter. Removed it and noted in the docstring why no periods factor is needed. Module is unreleased so there is no back-compat concern. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011a6EibcjDDv3Ff1Fb5esS1
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.
A new
overfitting.py— the flagship "honesty" differentiator surfaced by the research: tell the user when a backtest is likely a fluke. The mainstream Python stack (PyPortfolioOpt, quantstats, bt, vectorbt, pyfolio) ships none of this; this compounds quant_reporter's existing PSR / deflated-Sharpe / walk-forward lead.What
probability_of_backtest_overfitting(returns_matrix, n_splits=16, ...)PBOResult(pbo, logits, performance-degradation slope, prob-OOS-loss).min_track_record_length(returns, prob=0.95, ...)min_backtest_length(n_trials, sr_target_annual=1.0, ...)assess_overfitting(returns_matrix=…, returns=…, n_trials=…)robust/caution/likely_overfit/inconclusive) bundling PBO + deflated Sharpe + MinTRL →OverfittingReport.overfitting_section(report)Consume-only (you pass the trial matrix, e.g. from
backtest_manyover a param set), offline, no new dependencies; reusesperformance_stats' moment / deflated-Sharpe machinery.Honesty posture
Verdict thresholds are documented heuristics, overridable via
thresholds=, never asserted as decision rules — consistent with the "not-alpha" lane (the research refuted OOS-outperformance claims, so messaging stays on diagnostics).Tests (TDD, 22 unit tests)
Known-answer PBO: noise averages ~0.5 (the finite-N selection effect), a genuine edge → PBO < 0.3, a constructed overfit matrix → PBO > 0.7; MinTRL monotonic in confidence and
nanwhen the target is unreachable; MinBTL ↑ with trials, ↓ with target Sharpe; verdict wiring for every tier;overfitting_sectionrenders throughhtml_builder. Full suite: 445 passed, coverage 87.3%;ruff check src/clean.Docs
CHANGELOG.md(Added) andREADME.md(a "Backtest honesty (2.3)" row + subsection with examples) updated.Design spec:
docs/superpowers/specs/2026-07-01-backtest-honesty-design.md(local; that dir is gitignored).🤖 Generated with Claude Code
https://claude.ai/code/session_011a6EibcjDDv3Ff1Fb5esS1
Summary by Sourcery
Add a backtest-honesty module providing overfitting diagnostics and integrate it into the public API, documentation, and tests.
New Features:
Documentation:
Tests: