Skip to content

Commit 5e57944

Browse files
authored
feat: backtest-honesty module (PBO/CSCV, MinTRL/MinBTL, overfitting verdict) (#27)
New overfitting.py: probability_of_backtest_overfitting (PBO via CSCV), min_track_record_length, min_backtest_length, a one-call assess_overfitting verdict, and an embeddable overfitting_section. Consume-only, offline, no new deps. 22 TDD unit tests.
1 parent 66c7da4 commit 5e57944

5 files changed

Lines changed: 597 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ All notable changes to `quant_reporter` are documented here. This project follow
66
## [Unreleased]
77

88
### Added
9+
- **Backtest-honesty / overfitting diagnostics (`overfitting.py`).** A new module that
10+
flags when a backtest is likely a fluke: `probability_of_backtest_overfitting` (PBO via
11+
Combinatorially Symmetric Cross-Validation), `min_track_record_length` (MinTRL),
12+
`min_backtest_length` (MinBTL), and a one-call `assess_overfitting` verdict
13+
(`robust` / `caution` / `likely_overfit` / `inconclusive`) that bundles PBO with the
14+
deflated Sharpe ratio and MinTRL. `overfitting_section` renders the verdict as an
15+
embeddable report section. Consume-only, offline, no new dependencies. Refs: Bailey &
16+
López de Prado, *Deflated Sharpe Ratio* / *Probability of Backtest Overfitting*.
917
- **No-forecast allocation switch on the recommendation path** (GH #7). `recommend_weights`,
1018
`recommend`, and `walk_forward_recommendation` accept an opt-in `method=``"optimize"` (default,
1119
objective-based, uses expected returns) or the no-forecast allocators `"min_variance"`,

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ That's the whole loop: a `{ticker: weight}` dict in, an interactive report out.
5757
| *Which strategy actually holds up out-of-sample?* | **Strategy backtesting (2.1)**: cost-aware walk-forward `backtest`/`backtest_many`, honest PSR/DSR out-of-sample stats, interactive backtest report |
5858
| *What should I do about it?* (opt-in) | **Recommendation layer (2.1)**: recommended target weights, a rebalance trade list, risk-limit alerts, and a strategy verdict — each with its rationale & evidence |
5959
| *Does this suit my goals, and does it hold up out-of-sample?* (opt-in) | **Decision-support layer (2.2)**: a CFA-grounded investor `Profile` that constrains the optimizer and sets alert thresholds, plus walk-forward validation of the recommendation (in-sample vs OOS Sharpe + a holds-up / fragile verdict) |
60+
| *Is this backtest result real, or did I overfit picking it?* | **Backtest honesty (2.3)**: Probability of Backtest Overfitting (PBO via CSCV), Minimum Track Record / Backtest Length, and a one-call overfitting verdict that deflates the Sharpe for the number of trials you ran |
6061

6162
The 2.0 report generators are descriptive analytics on daily historical data — a decision-support and communication tool. **2.1 adds a cost-aware, walk-forward backtest engine, a composable strategy layer, and an opt-in recommendation layer** (see [Strategy backtesting & recommendations](#strategy-backtesting--recommendations-21)). Monte Carlo assumes Geometric Brownian Motion (thin tails — it understates crash risk), and reports depend on live `yfinance` data.
6263

@@ -276,6 +277,33 @@ The four pieces are also standalone — `qr.recommend_weights`, `qr.rebalance_tr
276277
`qr.risk_alerts`, `qr.compare_verdict` — and a recommendation can be embedded directly into a
277278
backtest report: `res.report("Backtest.html", recommendation=rec)`.
278279

280+
### Backtest honesty — is this result real, or overfit? (2.3)
281+
282+
When you try many strategy configurations and keep the best, its backtest is biased upward.
283+
This layer measures that bias directly. Pass the return streams of the configurations you
284+
tried (e.g. from `backtest_many` over a parameter set) and get a one-call verdict:
285+
286+
```python
287+
report = qr.assess_overfitting(returns_matrix=config_returns) # (T periods × N configs)
288+
report.verdict # 'robust' | 'caution' | 'likely_overfit' | 'inconclusive'
289+
report.pbo # Probability of Backtest Overfitting (CSCV) — how often the in-sample
290+
# winner lands at/below the out-of-sample median
291+
print(report.to_text())
292+
sections = [qr.overfitting_section(report)] # embeddable HTML report section
293+
```
294+
295+
The pieces are also standalone:
296+
297+
```python
298+
qr.probability_of_backtest_overfitting(config_returns, n_splits=16) # PBOResult
299+
qr.min_track_record_length(returns, prob=0.95) # observations needed for a significant Sharpe
300+
qr.min_backtest_length(n_trials=100, sr_target_annual=1.0) # years before N trials fake a Sharpe
301+
```
302+
303+
The thresholds behind the verdict are documented heuristics, not decision rules — read them
304+
alongside the strategy logic. Refs: Bailey & López de Prado, *Deflated Sharpe Ratio* and
305+
*The Probability of Backtest Overfitting*.
306+
279307
---
280308

281309
## Library (advanced) usage

src/quant_reporter/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,17 @@ def enable_logging(level=logging.INFO):
154154
compare_strategies_oos,
155155
)
156156

157+
# --- Backtest honesty (overfitting diagnostics) ---
158+
from .overfitting import (
159+
probability_of_backtest_overfitting,
160+
PBOResult,
161+
min_track_record_length,
162+
min_backtest_length,
163+
assess_overfitting,
164+
OverfittingReport,
165+
overfitting_section,
166+
)
167+
157168
# --- Walk-Forward (schedule unlock) ---
158169
from .validation_report import run_rolling_windows
159170

0 commit comments

Comments
 (0)