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 professional advice. This tool audits an analysis's testing process — it doesn't predict, recommend, or guarantee any outcome. Read the full disclaimer.
Principles shared by all nine modules
A verdict has five possible values. FAIL: a demonstrated defect, something concrete can be pointed at (a proven leak, a cost not applied, two engines that disagree). UNPROVEN (“not established”): no defect is demonstrated, but the evidence does not establish the claim — a Sharpe that does not survive the multiple-testing correction, an e-value under its threshold, a high PBO. Not proof the strategy is bad, and not a pass. WARN: nothing negative proven, but something could not be checked. PASS: everything that ran held. ERROR: the tool itself crashed — a bug in Elenchos, never a finding about your strategy. 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 a defect is demonstrated, UNPROVEN if a statistical claim is not established; otherwise WARN if there is a warning; otherwise PASS. The audit as a whole takes the worst module — FAIL, then UNPROVEN, then WARN, then PASS — except the audit self-check, which judges the tool: its failure prevents a PASS but never condemns your strategy.
Where the files go
For an audit passed through the MCP — or rebuilt by hand in the same tree:
<audit_folder>/
outputs_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 — input_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 |
Run it with the MCP
Elenchos installs its own MCP server with the app — there is nothing to configure. Open Elenchos, connect Claude or Codex, then ask it to audit a folder of results: it builds the layout above for you — creates the module subfolders, writes each field file in the right format, triggers the audit, and reads the PASS / WARN / FAIL register back with the evidence behind every finding.
It changes how you drive the audit, not where it runs. The server is a local process; your data, model and configuration never leave your machine, exactly as when you click through the tabs by hand.
The MCP does not change what the audit will and won't conclude. It can't invent a missing input — a specification grid, a reference baseline — so those still return WARN, and the module fields that take live Python objects rather than files still need a direct call.
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 model 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.
NOT ESTABLISHED (UNPROVEN) 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%. A statistical conclusion — the evidence does not establish skill — not a demonstrated defect.
02. Multiple testing
What it detects. Whether a reported headline metric 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 metric'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” score.
Computes the probability that the true value exceeds the maximum expected by pure chance after searching that many correlated trials, accounting for skew and kurtosis in the outputs (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_score reported_score.txt | optional | Otherwise recomputed from outputs_series.csv. |
| trial_scores trial_scores.csv | optional, required for DSR | Score 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. |
NOT ESTABLISHED (UNPROVEN) 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 metric, or if outputs are markedly non-normal. An impossible trial count (n_trials < 1) is refused, never computed. The verdict is conditional on the declared n_trials, which cannot be verified.
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 run 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 outputs_series with a warning that it “may contain in-sample data”. |
NOT ESTABLISHED (UNPROVEN) 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. A PACE gate with fewer than 20 discordant pairs is underpowered: WARN, not a rejection.
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 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-usable data (gapped or unavailable 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 four precise code patterns via the syntax tree, without ever executing the file: a shuffled train/test split on time-ordered data, a transformer (e.g. a scaler) .fit() before the split, a shuffling cross-validation splitter (KFold(shuffle=True), ShuffleSplit…), and a full-sample statistic (pandas or numpy mean / std / min / max…) applied to a variable before it is split. Not covered by this line: look-ahead through index shifts (shift(-k)), non point-in-time universes, undated fundamentals — lines 1 and 3 are the ones that address those.
| 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 — a demonstrated defect — 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 an analysis'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 model 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.
NOT ESTABLISHED (UNPROVEN) if none of the three joint tests rejects H0, or if the published specification sits above the 75th percentile of its own curve. Grid alternatives may themselves be lists (e.g. sets of weekdays).
06. Regime change
What it detects. Whether the strategy's performance, or its risk, changed during the audited period — which would invalidate every summary statistic computed over the whole period (the headline metric, PBO, DSR…), since those assume a homogeneous regime. Measured on simulations: ordinary volatility clustering does not trigger it (8–11% false alarms against a 10% target).
Bayesian Online Changepoint Detection (Adams & MacKay), on rank-based normal scores so that fat tails do not read as breaks. Estimates, at each instant, the probability distribution over “how long the current regime has lasted”. Purely descriptive.
Familywise error rate calibrated by simulation (Martin et al.). Reported for information only: its null assumes i.i.d. returns, so it fires on persistent volatility alone. Techniques D and E carry the verdict.
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”.
Did the mean return — hence the Sharpe — change? CUSUM on rank scores of returns divided by a causal EWMA volatility, normalised by a long-run variance, over the whole series and four sub-windows (Bonferroni-corrected), with Monte-Carlo thresholds. Measured on simulations: 79% detection of a Sharpe +2 → −0.5 switch, 93–98% of temporary episodes and offsetting breaks; a +1 → 0 switch is only caught about 30% of the time, an information limit rather than a defect.
Did the level of risk change beyond normal volatility clustering? κ2 test (Sansó, Aragó & Carrion) with a p-value from a parametric bootstrap under a GARCH(1,1) fitted to the series itself. Measured: 0–10% false alarms on persistent volatility, 80–92% detection of a ×1.25 to ×1.5 change.
| outputs_series outputs_series.csv (at the audit folder root) | needed for techniques A, B, D, E | Global return series — at least 100 values for D and E. Leading exact zeros (a warm-up period) are excluded from detection and reported. |
| regime_panel regime_panel.csv | optional (technique C) | Multi-asset matrix (T × N). Optional: everything else runs on the return series alone. |
NOT ESTABLISHED (UNPROVEN) if a performance break is detected (technique D): every single-number summary of the audit then averages two different performances. WARN if only a volatility break is detected (technique E): the risk taken changed, which is not a verdict on the strategy. The BOCPD alert (A, B) is descriptive and never sets the verdict.
07. Audit self-check
What it detects. Not the user's model — 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 crashes, is not reproducible, or violates a metamorphic relation — a demonstrated defect of the tool, and the failure names the property. NOT ESTABLISHED (UNPROVEN) if an injected defect escapes every retained relation on your inputs: the tool cannot prove it would catch it there. Either way this module judges the tool, never your strategy: its failure only prevents the audit as a whole from reading PASS.
08. Declared vs applied parameters
What it detects. Whether a parameter the analysis declares (a cost, penalty, threshold or adjustment) is both (A) plausible against a calibrated reference baseline, and (B) actually applied as stated in the computation. Two orthogonal defects: a pipeline can apply a perfectly correct calculation of an implausible value (A fails, B passes), or make an error applying a plausible one (A passes, B fails). Technique B follows the independent-reimplementation approach of Yin et al. (2026), shared with the implementation-risk module.
Compares each declared value against a reference baseline calibrated on real data, and flags a value that is implausibly small (below a hard floor, or below an “optimism ratio” band around the expected value). The module refuses to guess the baseline when it is not supplied — inventing one would produce a verdict worse than no verdict at all.
Comparison against an independent reference implementation — a minimal, auditable reimplementation. Independently recomputes the output series and headline metric from the raw inputs, and compares against what was actually reported — catching invisible bugs such as a scale factor accidentally divided by 100. A sign flip in the metric between the two (CSI) is the gravest possible finding.
| declared_value declared_value.txt | required | The parameter value the analysis states it used. |
| application_schedule application_schedule.csv | required for technique A | How much of the reference quantity the run touched in each interval. |
| baseline_params baseline_params.txt | required for technique A, never defaulted | Reference-baseline parameters calibrated on real data — an order of magnitude, not a universal truth for every dataset — or a raw dictionary. |
| input_weights / reference_prices / reported_output .csv each | required for technique B | The same files as implementation_risk — weights (T × N), reference series (T × N), reported output series. |
| applied_value applied_value.txt | required | The value the pipeline actually used (default 0). |
FAIL — a demonstrated defect — if the declared value is below the floor, if the sign of the metric flips (CSI), or if the reported output diverges from the reference by more than 1%. NOT ESTABLISHED (UNPROVEN) if the optimism ratio exceeds 2.0: that ratio comes from a calibrated model, not an identity. A divergence fully explained by the rebalancing convention (the reference rebalances to the target weights on every date, your engine only when they change) is reported as a named WARN, not a FAIL — supply the rebalancing dates to confirm it.
09. Implementation risk
What it detects. Whether two independently correct implementations would reach the same conclusion about a model — isolating errors caused by implementation choices alone (rounding, calendar handling, cost accounting) rather than by modelling 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 metric flips between two engines.
Compares, like a condition number, how much this particular analysis's complexity amplifies disagreement between engines relative to a minimal-complexity case — requires a second (minimal) case supplied separately.
| input_weights input_weights.csv | base | Matrix (T × N) of target weights. |
| reference_prices reference_prices.csv | base | Matrix (T × N) of closing prices. |
| reported_output reported_output.csv | base | The reported output series. |
| cost_rate cost_rate.txt | required | Proportional cost rate applied, as a fraction of the traded value (0.001 = 10 bp; default 0). |
| cost_rate_by_asset cost_rate_by_asset.csv | optional | One cost rate per asset (N values), same unit — takes precedence over cost_rate. Borrow cost on shorts is not modelled. |
Advanced fields are Python objects only, not plain files: zero_cost_reported_output (technique A), additional_engine_equities — a dict {label: series} (technique B), and minimal_complexity_weights / _prices / _reported_output (technique C, the DAF denominator).
FAIL — a demonstrated defect — if zero-cost agreement is broken, if the sign of the metric flips (CSI), or if the metric gap between engines exceeds 0.5.
Error log
When the audit self-check returns FAIL, UNPROVEN or ERROR, an error report is asked to be produced — a record of how the audit software behaved on your inputs, not of your analysis. 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 / UNPROVEN / FAIL / ERROR. |
| 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 model. |
| 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).
