ElenchosElenchosHome →

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.

RULE 00Not checked is not passed.

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 field

Each 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 typeExt.Expected content
matrix / array.csvA table of numbers — one column per asset or trial for a matrix, a single column for a vector
source.pyA 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.jsonA precise declarative structure, specific to each field
float / int / choice.txtA 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.

OVERFITTINGcombinatorial_audit

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”).

APBO / CSCV

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.

BE-value aggregation on CPCV splits

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.

CHarmonic p-value cross-check

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.

Fields to supply
perf_matrix
perf_matrix.csv
requiredMatrix (T rows = time × N columns = trials). Every trial, losers included: a hand-sorted list destroys the measurement.
split_pvalues
split_pvalues.csv
optionalOne p-value per CPCV split (single column).
candidate_pvalues
candidate_pvalues.csv
optionalMatrix (M candidates × K splits) — triggers e-BH selection across several candidates.
event_ends
event_ends.csv
optionalIndex 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.

Verdict

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%.

OVERFITTINGmultiple_testing

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.

ASharpe haircut (Harvey & Liu)

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.

BDeflated Sharpe Ratio (Bailey & López de Prado)

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).

CGBS model on a corpus of published t-stats (Harvey & Liu 2013)

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.

Fields to supply
n_trials
n_trials.txt
required, no defaultNumber 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
optionalOtherwise recomputed from returns_series.csv.
trial_sharpes
trial_sharpes.csv
optional, required for DSRSharpe of every trial, including abandoned ones.
trial_pvalues
trial_pvalues.csv
optionalRelaxes the haircut's default pessimistic assumption.
trial_correlation
trial_correlation.csv
optionalMatrix or scalar — converts M correlated trials into N effectively independent ones.
published_tstats
published_tstats.csv
optional (technique C)Corpus of published t-stats.
Verdict

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.

OVERFITTINGsequential_testing

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.

APACE validation gate

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.

BOptional-stopping / alpha-spending bounds

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.

CAlways-valid continuous monitoring

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.

Fields to supply
candidate_pnl / incumbent_pnl
.csv each
optionalPaired PnL on the same index, for the PACE gate.
pace_candidates
pace_candidates.json
optionalList 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
optionalOut-of-sample series; by default falls back to returns_series with a warning that it “may contain in-sample data”.
Verdict

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.

DATA LEAKAGEleakage_detection

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.

1Static, formally proven check, no execution

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.

1bisAutomatic extraction from pandas by execution tracing

Reconstructs the same kind of term from a traced execution path. This is no longer a proof — it only covers the path actually exercised.

2Tradability mask contract (Du et al.)

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.

3Metamorphic testing (safety net)

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).

4Operation-order analysis by AST on the raw script

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.

Fields to supply
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.

Verdict

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).

DATA LEAKAGEresearcher_dof

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.

ASpecification-curve analysis

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.

BJoint test across the specification family

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).

CPercentile of the published specification within its own curve

Checks whether the combination chosen for publication is a typical result of the family, or its most flattering exception — the signature of selective reporting.

Fields to supply
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 requiredOne column per grid combination, in grid order.
specification_runner
specification_runner.json
alternative to the aboveJSON {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.

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.

STATISTICAL RELIABILITYregime_monitoring

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.

ASegment structure via BOCPD

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.

BAlert threshold calibrated by sFWER

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.

CDependence structure via spectral features

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”.

Fields to supply
returns_series
returns_series.csv (at the audit folder root)
needed for techniques A and BGlobal return series.
regime_panel
regime_panel.csv
optional (technique C)Multi-asset matrix (T × N). Optional: everything else runs on the return series alone.
Verdict

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.

STATISTICAL RELIABILITYself_validation

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.”

AReproducibility

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.

BMetamorphic relations (“MR-Cards”)

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.

CDetection power by mutation

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.

Verdict

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.

COSTS & EXECUTIONtransaction_cost_filter

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.

ACost plausibility

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.

BCalculation reliability

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.

Fields to supply
declared_cost_bps
declared_cost_bps.txt
requiredIn basis points per share traded.
participation_schedule
participation_schedule.csv
required for technique AShare of market volume taken in each interval.
market_impact_params
market_impact_params.txt
required for technique A, never defaultedOne 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 BThe same files as implementation_risk — weights (T × N), prices (T × N), reported equity curve.
cost_rate
cost_rate.txt
requiredThe rate the backtest actually used (default 0).
Verdict

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%.

COSTS & EXECUTIONimplementation_risk

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.

ACausal isolation by zero-cost ablation

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).

BDivergence across an engine panel (K ≥ 2 engines)

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.

CDivergence Amplification Factor (DAF)

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.

Fields to supply
portfolio_weights
portfolio_weights.csv
baseMatrix (T × N) of target weights.
portfolio_prices
portfolio_prices.csv
baseMatrix (T × N) of closing prices.
reported_equity
reported_equity.csv
baseThe reported equity curve.
cost_rate
cost_rate.txt
requiredProportional 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).

Verdict

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.

MISCELLANEOUSself_validation

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_versionVersion number of the report format (currently 1).
environmentPython 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.
moduleAlways self_validation, never another module.
verdictPASS / WARN / FAIL.
metricsThe 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.
relationsFor each metamorphic relation (MR-8, MR-12…): the verdict it returned, as purged text.
coverageFor 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_casesThe cost of faults left uncovered by the time budget.
warnings / violationsFree text, but purged — see below.
Explicitly excluded
  • 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).