Documentation
How the audit works.
What to put in each module, in which folder, with which columns — and what each module actually verifies, named technique by technique.
Educational content. For education only, not financial advice. This tool audits a strategy's testing process — it doesn't predict, recommend, or guarantee any outcome. Read the full disclaimer.
Principles shared by all nine modules
A verdict always has three possible values: PASS, WARN, FAIL. One universal rule, restated in every module: a check that could not be run never produces a PASS. If no sub-check could run for lack of an input, the verdict is WARN — never PASS by default, never silent.
General logic: WARN if nothing was checked; otherwise FAIL if there is a violation; otherwise WARN if there is a warning; otherwise PASS.
Where the files go
For an audit passed through the MCP — or rebuilt by hand in the same tree:
<audit_folder>/
returns_series.csv ← optional, global, shared by several modules
<module_id>/
<field_key>.<extension> ← one file per module fieldEach module has its own subfolder (combinatorial_audit/, multiple_testing/, and so on). A field shared between two modules — portfolio_weights, for instance — must be duplicated in both subfolders.
Expected extension by field type
| Field type | Ext. | Expected content |
|---|---|---|
| matrix / array | .csv | A table of numbers — one column per asset or trial for a matrix, a single column for a vector |
| source | .py | A Python file — read differently per module (text/AST only, or imported and executed; stated module by module) |
| term_json, mask_json, metamorphic_json, runner_json | .json | A precise declarative structure, specific to each field |
| float / int / choice | .txt | A single raw value, on a single line |
Check reference
Nine independent modules. Each states what it verifies, what it needs, and how to read its verdict.
01. Combinatorial audit
What it detects. Whether picking the best trial out of many strategy variants is statistically different from picking at random — i.e. overfitting by selection (“run 50 variants, show only the winner”).
Probability of Backtest Overfitting via Combinatorially Symmetric Cross-Validation (Bailey / Borwein / López de Prado). Splits the trials' performance history into two halves across every possible combination, and asks whether the in-sample winner also tends to lose out-of-sample. PBO = 0.5 is equivalent to a coin flip.
Combines statistical evidence (“e-values”, a betting-based alternative to p-values) across many purged train/test splits, in a way that stays valid even when the splits are correlated with each other.
Locked behind an “empirical validity certificate” (Landau/stable-distribution calibration). Only activates if a SizeCertificate object is supplied — the module refuses to take on trust a general robustness property that has already been empirically refuted in a known case.
| perf_matrix perf_matrix.csv | required | Matrix (T rows = time × N columns = trials). Every trial, losers included: a hand-sorted list destroys the measurement. |
| split_pvalues split_pvalues.csv | optional | One p-value per CPCV split (single column). |
| candidate_pvalues candidate_pvalues.csv | optional | Matrix (M candidates × K splits) — triggers e-BH selection across several candidates. |
| event_ends event_ends.csv | optional | Index at which each observation actually resolves. Enables purging; no effect if labels are already point-in-time. |
A SizeCertificate (a Python object, not a file) can unlock technique C — not reachable by dropping a file in.
FAIL if PBO ≥ 0.20, or the global e-value (corrected across CPCV splits) < 1/α, or e-BH selection retains no candidate, or the out-of-sample loss probability exceeds 50%.
02. Multiple testing
What it detects. Whether a reported Sharpe ratio stays significant once you account for the real number of variants tried before arriving at the one being shown — the classic multiple-comparisons / data-dredging problem.
Recomputes the Sharpe's p-value after correcting for the number of trials (Bonferroni / Holm / BHY — Benjamini-Hochberg-Yekutieli), then converts the corrected p-value back into a “deflated” Sharpe.
Computes the probability that the true Sharpe exceeds the maximum expected by pure chance after searching that many correlated trials, accounting for skew and kurtosis in the returns (non-normality).
From a corpus of published t-statistics, reconstructs how many unpublished trials must actually have been run (publication bias), rather than trusting the trial count the researcher declares.
| n_trials n_trials.txt | required, no default | Number of trials attempted. Without it no correction is possible: the module refuses to assume “one trial”, which would reward the omission. |
| reported_sharpe reported_sharpe.txt | optional | Otherwise recomputed from returns_series.csv. |
| trial_sharpes trial_sharpes.csv | optional, required for DSR | Sharpe of every trial, including abandoned ones. |
| trial_pvalues trial_pvalues.csv | optional | Relaxes the haircut's default pessimistic assumption. |
| trial_correlation trial_correlation.csv | optional | Matrix or scalar — converts M correlated trials into N effectively independent ones. |
| published_tstats published_tstats.csv | optional (technique C) | Corpus of published t-stats. |
FAIL if the haircut destroys significance, or DSR < 0.95, or the publication-bias ratio exceeds 2.0. WARN if the haircut passes but destroys more than 50% of the Sharpe, or if returns are markedly non-normal.
03. Sequential testing
What it detects. Peeking bias — looking at results repeatedly and stopping as soon as they are significant — and repeated candidate-vs-incumbent comparisons. Optional-stopping / file-drawer bias in continuous or iterative backtest monitoring.
A sequential “e-process” test, valid at any stopping time (Shawn 2026 — unpublished, not peer-reviewed, single author, no code released; validated here only against a fixed McNemar baseline). Treats “does the candidate beat the incumbent” as a sequential betting game whose wealth process is a supermartingale under the null; Ville's inequality bounds the false-positive rate at any stopping time, even one chosen after looking at the data.
Lan-DeMets group sequential design, O'Brien-Fleming spending function by default. Computes how far the true false-positive rate is inflated by having looked at the results several times during the research.
E-value based test, normal-mixture and sign/median variants. Monitors an out-of-sample edge continuously with no fixed horizon, valid even if monitoring continues indefinitely — it loses in power what it gains in the right to keep looking.
| candidate_pnl / incumbent_pnl .csv each | optional | Paired PnL on the same index, for the PACE gate. |
| pace_candidates pace_candidates.json | optional | List of {name, candidate_pnl, incumbent_pnl} — several candidates trigger a run-level e-BH control. |
| interim_looks interim_looks.json | optional | {info_fractions, z_scores} — the real history of interim looks, for technique B. |
| oos_returns oos_returns.csv | optional | Out-of-sample series; by default falls back to returns_series with a warning that it “may contain in-sample data”. |
FAIL if the PACE gate is not passed, if a naively significant result does not survive the sequential correction, or if neither monitoring route (mean / median) rejects H0.
04. Data leakage
What it detects. Leakage in the training pipeline — the pipeline using information that would not have been available at the moment of the trading decision it simulates. Four independent lines of defence, each covering a different class of leak.
Fonseca's availability calculus and type-and-effect system. Your pipeline is written as a symbolic “term” in a small calculus that tracks, for every computed value, the first instant at which all its underlying data was genuinely available. A pipeline that passes this typing is mathematically proven free of look-ahead (any error can only be over-cautious, never a false positive). It never imports or executes code — it reads the term as pure data.
Reconstructs the same kind of term from a traced execution path. This is no longer a proof — it only covers the path actually exercised.
Detects a different class of leak: not future data, but non-tradable data (suspended or illiquid periods) silently absorbed into a rolling-window computation before a row filter removes them.
Actually runs the pipeline under systematically perturbed input scenarios and checks that outputs obey properties that must hold in the absence of leakage — a resilience test, never a proof of absence (Fonseca, Theorem 3: this is the theoretical optimum for the undecidable fragment; no complete automatic checker is possible).
Looks for two precise code patterns via the syntax tree, without ever executing the file: a shuffled train/test split on time-ordered data, and a transformer (e.g. a scaler) .fit() before the split.
| training_source training_source.py | optional (line 4) | Read as text, parsed to an AST, never imported or executed. |
| pipeline_term pipeline_term.json | optional (line 1, the strongest) | Your pipeline written as a term of the availability calculus — read as pure data. |
| masked_operators masked_operators.json | optional (line 2) | Point at your own module and function to audit your own code — that .py file is imported and executed. Referencing an operator from the built-in registry proves nothing about your pipeline. |
| metamorphic metamorphic.json | optional (line 3) | Points at your pipeline function and its inputs — the file is imported and EXECUTED. |
Practical limit: lines 1 and 4 are reachable by simply dropping in a file. Lines 2 and 3 actually take Python objects (operators, a (function, scenario) pair) that a file alone cannot always carry in full — in complex cases, going through the MCP (which can write the adapter code) or a direct Python call is needed to exploit them fully.
FAIL on any proven violation (line 1), any known leak found by tracing, any mask-contract failure, any violated metamorphic relation, or any high-severity AST finding (line 4).
05. Researcher degrees of freedom
What it detects. Whether a backtest's conclusion is an artefact of one particular (favourable) choice among many equally defensible analysis decisions — window length, filter thresholds, and so on. The “garden of forking paths” fragility.
Replays the same strategy under every combination of the declared choices, plots the distribution of resulting effects, and identifies which single decision explains the most variation.
Three statistics — median, significant share, Stouffer's Z — via a block bootstrap forcing H0. Tests whether the entire family of results, taken together, is incompatible with “no effect”, by resampling rather than an analytic formula (specifications are not independent).
Checks whether the combination chosen for publication is a typical result of the family, or its most flattering exception — the signature of selective reporting.
| specification_grid specification_grid.json | required, no default | {"decision": [alternatives]}, e.g. {"window": [20, 40, 60]} |
| specification_returns specification_returns.csv | one of the two required | One column per grid combination, in grid order. |
| specification_runner specification_runner.json | alternative to the above | JSON {module, function} pointing at your code — that .py file is imported and executed to replay each specification. |
| reported_specification reported_specification.json | optional (enables technique C) | {"decision": choice} — the combination actually published. |
By design the module never invents the specification grid — “only an expert, not an algorithm, can identify the set of theoretically justified and statistically valid analyses.” A missing grid always yields WARN, never an invented verdict.
FAIL if none of the three joint tests rejects H0, or if the published specification sits above the 75th percentile of its own curve.
06. Regime change
What it detects. Whether the market regime changed during the audited backtest period — which would invalidate every summary statistic computed over the whole period (Sharpe, PBO, DSR…), since those assume a homogeneous regime.
Bayesian Online Changepoint Detection (Adams & MacKay). Estimates, at each instant, the probability distribution over “how long the current regime has lasted”; a collapse in that most-likely run length signals an apparent break. Purely descriptive — no threshold applied here.
Familywise error rate calibrated by simulation (Martin et al.). Rather than an arbitrary threshold, Monte-Carlo simulates the null to measure empirically which threshold actually produces the intended false-alarm rate.
Optional, requires a multi-asset panel. Tracks the effective rank and spectral gap of a rolling cross-asset correlation structure; an effective rank near N means diversification is intact, near 1 means a single common factor absorbs everything — “diversification has evaporated”.
| returns_series returns_series.csv (at the audit folder root) | needed for techniques A and B | Global return series. |
| regime_panel regime_panel.csv | optional (technique C) | Multi-asset matrix (T × N). Optional: everything else runs on the return series alone. |
FAIL — not merely WARN — if the calibrated BOCPD statistic falls below the sFWER threshold. A detected regime change is escalated to FAIL precisely because it compromises the assumption every other module rests on.
07. Audit self-check
What it detects. Not the user's strategy — the audit software itself, tested on the user's real inputs rather than on a carefully chosen demo set. “A module can be correct on our test cases and broken on the user's data.”
Does each targeted module return bit-identical output when the same input is replayed with the same random seed? A prerequisite for everything else — without it, no relation violation can be distinguished from ordinary execution noise.
Checks whether necessary invariant properties of the targeted modules hold on the user's real inputs (e.g. scale invariance of a statistic). These relations were derived specifically for this project — no existing generic methodology provided relations for a statistical audit.
Known defects (“mutants”) are deliberately injected into the targeted modules, and the module checks whether the retained metamorphic relations still catch them. “A relation suite that passes while detecting nothing proves nothing.”
No fields to fill in manually. This module derives itself automatically from the inputs already supplied to the other modules in the same audit (for example, combinatorial_audit is targeted if perf_matrix or split_pvalues is present elsewhere in the folder). Explicit design choice: the module never fabricates a missing input — inventing a noise perf_matrix would validate the software on data unrelated to the user's.
FAIL if a targeted module is not reproducible, if a metamorphic relation is violated, or (by default) if an injected defect escapes every retained relation.
08. Transaction costs
What it detects. Whether the trading cost declared in a backtest is both (A) plausible given real market liquidity for the claimed trading volume, and (B) correctly computed and applied. Two orthogonal defects: “a backtest can apply a perfectly correct calculation of an unrealistic rate (A fails, B passes), or make an error applying a realistic rate (A passes, B fails).” After Busseti & Lillo (2012) for A, and “Algorithm 1” of Yin et al. 2026 for B — the same paper as the implementation_risk module.
Transient market-impact model of the Almgren-Chriss type, calibrated on real market data. From the share of market volume the strategy claims to have taken each period, computes the expected cost a real market would have charged, and flags a declared cost that is implausibly low (below the bid-ask spread alone, or below an “optimism ratio” band around the expected cost). The module explicitly refuses to guess market parameters — inventing them would produce a verdict worse than no verdict at all.
Comparison against an independent reference implementation — a minimal, auditable transcription of Yin et al.'s “Algorithm 1”. Independently recomputes the equity curve and Sharpe from raw weights, prices and cost rate, and compares against what was actually reported — catching invisible bugs such as a commission rate accidentally divided by 100. A Sharpe sign flip between the two (CSI) is the gravest possible finding.
| declared_cost_bps declared_cost_bps.txt | required | In basis points per share traded. |
| participation_schedule participation_schedule.csv | required for technique A | Share of market volume taken in each interval. |
| market_impact_params market_impact_params.txt | required for technique A, never defaulted | One of (none), AZN, VOD, AAPL, AMZN — parameters calibrated on four LSE/NASDAQ large caps, 2000-2002 and 2009: an order of magnitude, not a truth for a small-cap or crypto market — or a raw dictionary. |
| portfolio_weights / portfolio_prices / reported_equity .csv each | required for technique B | The same files as implementation_risk — weights (T × N), prices (T × N), reported equity curve. |
| cost_rate cost_rate.txt | required | The rate the backtest actually used (default 0). |
FAIL if the declared cost is below the spread floor, if the optimism ratio exceeds 2.0, if the sign of the Sharpe flips (CSI), or if the reported return diverges from the reference by more than 1%.
09. Implementation risk
What it detects. Whether two independently correct backtest implementations would reach the same conclusion about a strategy — isolating errors caused by implementation choices alone (rounding, calendar handling, cost accounting) rather than by trading logic. After Yin, Miki, Lesnichenko & Gural (2026), arXiv:2603.20319.
At zero transaction cost, two correct engines must agree exactly. This test conditions the interpretation of everything else — if it fails or is absent, no divergence measured at real cost can be attributed to the cost model alone (it could come from a calendar, alignment, or valuation bug).
Four metrics, including CSI (Conclusion Sign Inversion) — the only threshold-free indicator: it blocks by construction if the sign of the Sharpe flips between two engines.
Compares, like a condition number, how much this particular backtest's complexity amplifies disagreement between engines relative to a minimal-complexity case — requires a second (minimal) case supplied separately.
| portfolio_weights portfolio_weights.csv | base | Matrix (T × N) of target weights. |
| portfolio_prices portfolio_prices.csv | base | Matrix (T × N) of closing prices. |
| reported_equity reported_equity.csv | base | The reported equity curve. |
| cost_rate cost_rate.txt | required | Proportional cost rate applied (default 0). |
Advanced fields are Python objects only, not plain files: zero_cost_reported_equity (technique A), additional_engine_equities — a dict {label: series} (technique B), and minimal_complexity_weights / _prices / _reported_equity (technique C, the DAF denominator).
FAIL if zero-cost agreement is broken, if the sign of the Sharpe flips (CSI), or if the Sharpe gap between engines exceeds 0.5.
Error log
When the audit self-check returns FAIL, an error report is asked to be produced — a record of how the audit software behaved on your inputs, not of your strategy. Its contents are fixed and auditable: everything in the report is listed below, and everything outside that list is excluded by construction.
What the report contains| report_version | Version number of the report format (currently 1). |
| environment | Python version, OS/version, report version; software version is explicitly “not declared” — no version constant exists in the repository, so no fake 1.0 is invented. |
| module | Always self_validation, never another module. |
| verdict | PASS / WARN / FAIL. |
| metrics | The 18 real self_validation metrics, taken whole — counters (mr_passed, n_targets…), execution time, replay floor. Safe by construction: they describe the tool's behaviour, never the strategy. |
| relations | For each metamorphic relation (MR-8, MR-12…): the verdict it returned, as purged text. |
| coverage | For each seeded fault: which bucket it falls into — detected / undetected / no_signal / relation_without_margin — using only names from the internal catalogue (MR-x, Fxx). |
| uncovered_cases | The cost of faults left uncovered by the time budget. |
| warnings / violations | Free text, but purged — see below. |
- The other eight modules' reports never enter this payload — excluded by construction rather than by filter, so there is no risk of a gap.
- Every number in the free text (warnings / violations) is replaced by [value] — except catalogue identifiers (MR-8b, F15), which are kept because without them the report is unusable.
- An exception's raw message is truncated immediately after its type (e.g. ValueError) and the rest discarded — this is the only real leak vector identified, since an exception message can quote user data without containing a single digit, such as a column name.
- No network send by default: ENDPOINT = None as long as no collection server is connected. The “Send” button says honestly that it sent nothing, rather than failing silently.
- The report is always shown to the user before any write or send (the apercu() function, full JSON, never a partial summary).
