Changelog¶
All notable changes to purged-cross-validation are recorded here. The
format is based on Keep a Changelog.
The 0.0.x series is the pre-1.0 development line. The library's feature
work was organised internally as Plans A–D (foundations → splitters →
backtest paths → metrics); the cumulative feature set delivered by each
Plan is listed under the published version it shipped in.
Unreleased¶
0.1.2 - 2026-06-13¶
Documentation¶
- Quickstart now shows how to pass
sample_weightthrough the splitters. Because the splitters stay drop-in to scikit-learn and ship no scorer of their own, weights travel via sklearn metadata routing (enable_metadata_routing,set_fit_request/set_score_request); the section covers both train-time-only weighting and weighting the score through a custom scorer, and theUnsetMetadataPassedErrorit raises if the scorer neither requests nor declines the weight. path_metricsdocstring states themax_drawdownsign convention at the point of use (positive magnitude; worst path isidxmax, notidxmin), matching the note already indefault_backtest_metrics.
0.1.1 - 2026-06-05¶
Added¶
minimum_backtest_length(MinBTL): the backtest length, in years, below which a reported annualised Sharpe is within what selecting the best ofn_trialsindependent backtests produces by chance under the null. It is the inverse of the Deflated Sharpe Ratio deflation, reusing the same Bailey-Lopez de Prado expected-maximum bracket term, and finishes the AFML Chapter 11 selection-bias toolset (PSR, DSR, PBO, MinTRL) in-package. Implements Bailey, Borwein, Lopez de Prado & Zhu (2014), "Pseudo-mathematics and financial charlatanism".
0.1.0 - 2026-06-05¶
First minor release. The version bump from the 0.0.x line signals that
the public API in purgedcv/__init__.py is now considered usable and is
maintained deliberately: the splitters, backtest-path reconstruction, and
the Sharpe-ratio metric family are stable enough to build on. Breaking
changes will still happen before 1.0.0, but they will be noted here
rather than slipped into a patch release.
Added¶
probability_of_backtest_overfitting(PBO): estimates how often the configuration chosen as best in-sample lands below the median out-of-sample, using Combinatorially Symmetric Cross-Validation (CSCV). Returns a frozenPBOResultdataclass (read fields by attribute): the PBO value, the per-combination logits, the IS-versus-OOS performance-degradation slope, and the IS/OOS performance pairs. When prediction and evaluation times are supplied it cleans every IS/OOS boundary with the existing purge and embargo machinery. Implements Bailey, Borwein, Lopez de Prado & Salehipour (2017) and AFML Chapter 11.CombinatoriallySymmetricCV: the CSCV splitter that PBO is built on, exposed directly. It isCombinatorialPurgedCVwithn_test_groups = n_splits // 2(evenn_splitsrequired).deflated_sharpe_ratio_full: returns a frozenDSRDiagnosticsdataclass with the Deflated Sharpe probability alongside the quantities that explain it (observed Sharpe, deflated benchmarksr_star, the standardized expected-maximum multiplierexpected_max_z,var_sharpe,n_trials, track-record length, skew, and kurtosis). The scalardeflated_sharpe_ratiokeeps its scalar return. Both gained an optionalbars_per_year:var_sharpeis per-observation by default, and passingbars_per_yearconverts an annualised Sharpe variance to per-observation internally. This closes the unit trap wherepath_metrics(..., bars_per_year=...)returns an annualised Sharpe whose variance does not match the per-observation contract DSR requires.path_metricsanddefault_backtest_metrics: reduce an(n_paths, n_samples)CPCV path matrix to a per-path DataFrame of Sharpe, Calmar, max drawdown, and total return in one call.CombinatorialPurgedCV.reconstruct_paths(fold_predictions): an instance method that assembles per-fold predictions into backtest paths without the caller restatingn_splits,n_test_groups, orn_samples. Complements the existing freereconstruct_pathsfunction.purgedcv.optuna_integration.TrialSharpeRecorder: an Optuna study callback that collects per-trial Sharpe ratios and reports thevar_sharpeand trial count thatdeflated_sharpe_rationeeds, plusn_effective()for the autocorrelation-adjusted count. Importing the module does not require Optuna; theoptunaoptional extra (pip install purgedcv[optuna]) installs it for the surrounding optimisation loop.effective_n_trials: estimates the number of independent trials behind a correlated search (TPE, CMA-ES) from the integrated autocorrelation time of the trial-performance series. Pass it todeflated_sharpe_ratioso a 6000-trial TPE run is not deflated as if all 6000 were independent, which otherwise crushes DSR to zero. Documented as a heuristic.- New example
examples/optuna_dsr_cookbook.py: the canonical end-to-end Optuna + Deflated Sharpe pattern (record per-trial Sharpe, deflate by the effective trial count, convert annualisedvar_sharpeviabars_per_year) that most users will write. - New example notebook
examples/backtest_overfitting_audit.ipynb: a seeded Optuna TPE search over a Ridge strategy on real BTC/USDT daily data, audited end to end with PBO,effective_n_trials,deflated_sharpe_ratio_full(withbars_per_year), andCombinatorialPurgedCV.backtest_pathspluspath_metrics. The search reaches an in-sample Sharpe of +2.5; the audit shows PBO 0.55, the 400 TPE trials worth about 25 independent bets, and a modest deflated Sharpe, while the per-path spread confirms the model family does carry real structure on this trending window. optunaadded to theexamplesoptional-dependency extra so the gallery runs withpip install purgedcv[examples].- Python 3.13 and 3.14 are now part of the CI test matrix and listed among the supported versions.
- New example notebook
examples/selection_regret_lcl.ipynb: on UK Low Carbon London smart-meter data,PurgedGroupKFoldpicks a model that deploys with 5 to 12% lower MAE on 12 truly unseen households than naive shuffledKFolddoes. Holds across 5 random seeds. Companion tosynthetic_leakage_proof.ipynb: same library, opposite framing (negative proof of fabricated skill there; positive demonstration of selection regret here). - New example notebook
examples/selection_regret_crypto.ipynb: same selection-regret protocol on daily BTC/USDT 2021-2023. The naive selector picks a deep RandomForest with apparent CV MAE 0.057; on 180 truly held-out bars it deploys at R² = -1.64 and Sharpe = -0.77.PurgedKFoldpicks Ridge withalpha=100(R² = +0.01, Sharpe = -0.26 on the same bars). Both strategies lose money over the sideways-down deployment window, but the naive pick loses three to five times more per unit of risk. The library's value on a no-edge market is loss avoidance rather than gain.
Changed¶
min_track_record_lengthnow returnsmath.infinstead of raising whenobserved_sharpe <= target_sharpe: no finite track record can establish a gap that is not there, and infinity is the well-defined answer. The return type is nowfloat(wrap inint(...)for a count when finite). Other input validation (alpharange, non-finite inputs) still raises.validate_timesnow requires a datetime-like or timedelta-like dtype forprediction_timesandevaluation_times. Numeric, string, and object-dtype series are rejected; convert withpd.to_datetimefirst.parse_horizonrejectsNaTand other missing horizons.- The splitter constructors and
assert_groups_disjointreject missing (NaN) group labels instead of grouping onNaN. purgeandapply_embargovalidate the horizon before the empty-index short-circuit, so a bad horizon fails fast even when the train or test set is empty.- The automated release job now runs only when a merge changes the
shipped package (
src/orpyproject.toml); documentation, CI, tooling, test, and example changes no longer publish a release. This generalises the documentation-only skip rule introduced in 0.0.9.
Fixed¶
deflated_sharpe_ratioreturned1.0for every input whenn_trials=1. The single-trial branch set the deflated benchmark SR* to-inf; it now uses SR* = 0, so DSR reduces toprobabilistic_sharpe_ratio(returns, 0.0). A losing strategy is no longer reported as certain skill. Covered by a regression test.WalkForwardSplit(window="sliding", train_size=0)silently used the entire history as the train window, becausearr[-0:]selects the whole array.train_sizeis now validated as a positive integer.purgeandapply_embargosilently accepted negative orNaThorizons; they now raiseValueError.reconstruct_pathsacceptedfold_test_indicesthat did not match the canonical CPCV group layout and produced a wrong path matrix; it now verifies the layout and rejects mismatches.CombinatorialPurgedCVacceptedn_splits > n_samples, which builds empty group blocks; this is now rejected at construction.- The metrics functions accepted non-finite inputs (infinite or
NaNreturns,benchmark_skill,var_sharpe, and the scalar arguments ofmin_track_record_length) and non-integern_trials, producing meaningless probabilities. They now raise.min_track_record_lengthwithalpha >= 0.5no longer returns an inflated length. - The splitter integer parameters (
n_splits,test_size,train_size,n_test_groups) are validated at construction; floats and booleans are rejected withTypeError.
0.0.9 - 2026-05-22¶
No library code changed in this release; the published wheel is identical to v0.0.7. The release carried CI and example tooling only.
Added¶
- The CI workflow detects whether a push touched files outside the
documentation surface and skips the automated release job for
documentation-only merges to
main, so editing documentation no longer publishes a PyPI release or bumps the version.
Fixed¶
- Import ordering in
examples/synthetic_leakage_proof.ipynb(ruffI001).
0.0.8 - 2026-05-21¶
Intermediate empty auto-release patch. No library, CI, or documentation changes.
0.0.7 - 2026-05-20¶
This project publishes a patch release automatically on every push to
main. Versions 0.0.5 and 0.0.6 were intermediate same-day auto-release
patches; this entry records the work delivered across 0.0.5 through
0.0.7.
Added¶
- JOSS submission paper at
docs/paper.md(docs/paper.bib) with the real ORCID baked in. - Hosted documentation site at
https://eslazarev.github.io/purged-cross-validation/ using
MkDocs Material + mkdocstrings, deployed by
.github/workflows/docs.yml. The PR check inci.ymlrunsmkdocs build --strict. - Community files required for JOSS verification:
CITATION.cff,CONTRIBUTING.md,CODE_OF_CONDUCT.md(Contributor Covenant 2.1),.zenodo.json, issue and pull-request templates. - Full-population UK Low Carbon London benchmark
(
tools/lcl_full_benchmark.py) — chunked enumeration over the raw ~8 GB corpus, K seeded subsamples of N households, mean ±95% t-interval. Real result (K=20, N=60, seed=0, 4,284 eligible households): temporal leak 1.60% (95% CI 1.27 – 1.94%), household leak 6.03% (95% CI 4.93 – 7.12%). End-to-end testtest_e2e_lcl_full_benchmark.py. - Controlled competitor benchmark (
tools/competitor_benchmark.py) and the empirical write-up atpaper/competitor_benchmark.md— on the controlled taskpurgedcvadmits 0.000 label overlap while defaultKFold(shuffle=True)fabricates R² = +0.92; mlfinpy is shown to be broken on pandas ≥ 2.0; RiskLabAI does not install on a modern stack. End-to-end testtest_e2e_competitor_benchmark.py. context7.jsonso the docs are indexable by Context7.
Changed¶
BaseTemporalSplitternow requires monotonicprediction_timesat construction (require_monotonic=True). Closes a silent train-from-future failure mode inWalkForwardSplit.purge,apply_embargo, and the diagnostics no longer collapse separated test blocks into one global interval. The newsrc/purgedcv/_intervals.pyprovidesoverlaps_any_half_open_intervalandpoints_in_any_closed_interval; both filters operate on the union of local per-row intervals. Fixes CPCV folds with non-adjacent test groups and the artificial-NaN paths (energy_demand_pjmnotebook now reports 0.0% NaN paths instead of 100%).- Embargo is now applied per test row (
[eval_i, eval_i + embargo]unioned across the fold) rather than only aftermax(eval). For contiguous PurgedKFold this is strictly more conservative; for CPCV with non-adjacent groups it is the correct generalisation.
Fixed¶
- Version desync between
pyproject.tomlandsrc/purgedcv/__init__.py. The release workflow now bumps both files (alpha-aware), and the new install-smoke testtest_packaging_metadata_versions_match_runtimefails CI if they drift again.
0.0.4 - 2026-05-17¶
By v0.0.4 the cumulative feature set of Plans A through D below was delivered.
Plan D — Domain D7: statistical metrics¶
probabilistic_sharpe_ratio(returns, benchmark_skill)— probability that the true Sharpe ratio exceeds a benchmark, corrected for non-normality (Bailey & López de Prado 2012).deflated_sharpe_ratio(returns, n_trials, var_sharpe)— PSR adjusted for multiple-comparison bias across independent strategy evaluations (Bailey & López de Prado 2014).min_track_record_length(observed_sharpe, target_sharpe, alpha, skew, kurtosis)— minimum sample size required for PSR to exceed1 − alphaat the observed Sharpe; the analytical inverse of PSR.
Plan C — Domain D6: CPCV backtest path reconstruction¶
reconstruct_paths(fold_predictions, fold_test_indices, n_splits, n_test_groups, n_samples)— pure function that combines the C(N,K) fold outputs into the C(N−1,K−1) canonical backtest paths described in López de Prado (2018) chapter 12.CombinatorialPurgedCV.backtest_paths(estimator, X, y)— convenience method that runs the full CPCV fit-predict loop and callsreconstruct_paths, returning an(n_paths, n_samples)float array withNaNfor unseen observations.
Plan B — Domains D4 + D5: splitter framework¶
BaseTemporalSplitter(D4) — abstract base class wiringpurge+apply_embargointo every fold produced by concrete subclasses; enforces group disjointness whengroupsare supplied.WalkForwardSplit(D5.1) — sliding-window and expanding-window walk-forward CV; configurabletrain_size,test_size, andstep;with_timesadapter for fluent construction.PurgedKFold(D5.2) — contiguous test folds tiling the index space, with purge and embargo applied; degrades to standardKFold(shuffle=False)at zero purge/embargo.PurgedGroupKFold(D5.3) — group-aware variant ofPurgedKFoldthat assigns whole groups to folds so no entity leaks across the train/test boundary.CombinatorialPurgedCV(D5.4) — exhaustive C(N, K) combinatorial fold enumeration for producing multiple backtest paths from a single dataset.- scikit-learn integration: every splitter satisfies the
sklearn.model_selectionsplitter protocol and works insidecross_val_score,GridSearchCV, andPipeline.
Plan A — Foundations: Domains D1 + D2 + D3 + D8¶
parse_horizon,horizons_overlap,validate_times(D1) — time and horizon utilities; strict validation of monotonicity, NaN-freedom, and chronological ordering.purge(D2) — drops training rows whose half-open label horizon[prediction_time, evaluation_time)overlaps the test horizon; implements AFML Section 7.4.1 Snippet 7.1.apply_embargo(D3) — drops training rows whoseprediction_timefalls in the post-test asymmetric embargo window[test_eval_max, test_eval_max + embargo].purgedcv.diagnosticssubmodule (D8) —assert_no_temporal_leakage,assert_embargo_respected,assert_groups_disjoint,compute_overlap_fractionfor auditing custom splits.- Exception hierarchy —
TemporalCVError,TemporalLeakageError,EmbargoViolationError,GroupLeakageError. - src-layout Python package using hatchling as the build backend.
- TDD test suite with hypothesis property tests; ruff + mypy strict + pre-commit quality gates.
Examples (added across 0.0.x development)¶
- Worked-example notebook gallery on real public data: PhysioNet ICU mortality, NASA C-MAPSS turbofan RUL, NOAA GHCN-Daily rainfall, PJM hourly load, Binance BTC/USDT, UK Low Carbon London smart meters, USGS earthquakes, UCI air quality, Premier League matches, and a controlled synthetic leakage proof.
0.0.3 - 2026-05-16¶
Development patch release.
0.0.2 - 2026-05-16¶
Development patch release.
0.0.1 - 2026-05-16¶
First PyPI release.