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

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>/
  outputs_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 — input_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

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.

OVERFITTINGcombinatorial_audit

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

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

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.

OVERFITTINGmultiple_testing

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.

ASharpe haircut (Harvey & Liu)

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.

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

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

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_score
reported_score.txt
optionalOtherwise recomputed from outputs_series.csv.
trial_scores
trial_scores.csv
optional, required for DSRScore 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

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.

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 run 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 outputs_series with a warning that it “may contain in-sample data”.
Verdict

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.

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

2Availability mask contract (Du et al.)

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.

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

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

DATA LEAKAGEresearcher_dof

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.

ASpecification-curve analysis

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.

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

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

STATISTICAL RELIABILITYregime_monitoring

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

ASegment structure via BOCPD

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.

BBOCPD alert, sFWER-calibrated (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.

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

DPerformance break

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.

EVolatility break

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.

Fields to supply
outputs_series
outputs_series.csv (at the audit folder root)
needed for techniques A, B, D, EGlobal 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.
Verdict

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.

STATISTICAL RELIABILITYself_validation

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

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

ASSUMPTIONS & REPRODUCIBILITYparameter_check

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.

AValue plausibility

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.

BCalculation reliability

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.

Fields to supply
declared_value
declared_value.txt
requiredThe parameter value the analysis states it used.
application_schedule
application_schedule.csv
required for technique AHow much of the reference quantity the run touched in each interval.
baseline_params
baseline_params.txt
required for technique A, never defaultedReference-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 BThe same files as implementation_risk — weights (T × N), reference series (T × N), reported output series.
applied_value
applied_value.txt
requiredThe value the pipeline actually used (default 0).
Verdict

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.

ASSUMPTIONS & REPRODUCIBILITYimplementation_risk

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.

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 metric flips between two engines.

CDivergence Amplification Factor (DAF)

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.

Fields to supply
input_weights
input_weights.csv
baseMatrix (T × N) of target weights.
reference_prices
reference_prices.csv
baseMatrix (T × N) of closing prices.
reported_output
reported_output.csv
baseThe reported output series.
cost_rate
cost_rate.txt
requiredProportional 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
optionalOne 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).

Verdict

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.

MISCELLANEOUSself_validation

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_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 / UNPROVEN / FAIL / ERROR.
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 model.
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).