Skip to content

Measure the edge before you ever open a $100k account.

Most trading "edges" are artifacts of a small sample. crucible takes a trade log and tells you, with a confidence interval and a p-value, whether the edge is real. No account, no position sizing, no equity curve. It's the thing you run before a backtester.

Get started Read the tutorial View on GitHub

Run modes Visualizations The gauntlet (design) Architecture

Quickstart

pip install crucible              # core: metrics + stats + simulator (numpy/pandas only)
pip install "crucible[examples]"  # + yfinance, to run the demo below on real data

Installed as crucible, imported as crucible (import crucible).

The 30-second example

import yfinance as yf
from crucible.edge import barrier_trades, edge_report, reality_check
from crucible.strategies import ma_cross

px = yf.download("ES=F", start="2010-01-01")        # any OHLC frame works
entries = ma_cross(px, fast=20, slow=50)            # your signal: a boolean Series

trades = barrier_trades(px, entries, side="long",   # signal -> trade log (in R)
                        tp=2.0, sl=1.0, timeout=20) # 2R target, 1R stop, 20-bar cap

print(edge_report(trades))                          # the full capital-free scorecard
print(reality_check(trades))                        # <-- the verdict
======================================================
 EDGE REPORT (capital-free)
======================================================
Trades              : 214
Win rate            : 38.3 %
------------------------------------------------------
Expectancy          : +0.081 R      [PASS]
Profit factor       : 1.34          [PASS]
Payoff ratio        : 2.16          [INFO]
SQN-100             : 1.72          [INFO]
------------------------------------------------------
Excursion ratio     : 1.28          [PASS]
======================================================

VERDICT (expectancy): +0.081 R   95% CI [-0.031, +0.196]
                     p(edge>0) = 0.071        ->  FRAGILE
  point positive, but the CI straddles zero, not distinguishable
  from noise at this sample size. Do NOT size it up.

That FRAGILE block is the whole point: a positive expectancy that a backtester would have shown you as a rising equity curve is, at this sample size, indistinguishable from noise. crucible says so out loud.

What crucible answers

From a single trade log up to the whole correlated book, one question at a time, each harder than the last, each answered out loud by a named function:

  • Describe the edge


    Expectancy, profit factor, payoff, SQN, MFE/MAE efficiency — the capital-free scorecard.

    edge_report

  • Quantify sampling noise


    A bootstrap CI and a p-value on every headline metric, so a point estimate becomes a defensible claim.

    bootstrap_ci · p_value_positive

  • Rule out data-mining luck


    Sign-permutation p-value (Masters) plus a random-entry reality check: could no-edge-at-all have produced this?

    permutation · reality_check

  • Rule out drift


    A leakage-controlled early-train / late-confirm split, with purge and embargo hygiene at the boundary.

    validation.holdout

  • Confirm out-of-sample


    Pardo walk-forward: optimize in-sample, confirm out-of-sample, stitch the honest log.

    validation.walk_forward

  • Price the search itself


    How much did selecting the best config overfit? Probability of Backtest Overfitting (CSCV) and the deflated Sharpe.

    validation.pbo

  • Account for correlation


    The effective number of independent bets across a correlated book — the honest denominator for significance.

    breadth.effective_n

  • …then all of it, as one gate


    Every check above as an ordered, audited gauntlet that passes only if every gate does.

    validation.run_gauntlet

One verdict for the whole edge

The individual tools each answer one question. The gauntlet runs them as an ordered set of hard gates — REAL (not noise, corrected for the search) → STRONG (real at the CI lower bound) → DURABLE (holds out-of-sample) → GENERAL (travels across markets) — and returns a single audited pass/fail.

from crucible.validation import run_gauntlet

gauntlet = run_gauntlet(
    wf.stitched,        # the honest log, stitched out-of-sample
    prices=px,          # enables REAL's random-timing null
    wf=wf,              # adds the DURABLE gate
    n_variants=4,       # size of your search -> REAL's Šidák correction
)
print(gauntlet.audit_report())
print(gauntlet.passed)  # True only if every gate that ran passed

How the gauntlet is designed →

What this is, and isn't

✅ Trade-level edge metrics, excursion efficiency, bootstrap CIs, a random-entry reality check, all capital-free. Costs are in scope: test on r net of your commission and slippage — the tutorial provides a simple per-trade cost model for the netting, or supply your own.

❌ No capital, position sizing, CAGR, drawdown, or Monte-Carlo-on-equity. If you want an equity curve, hand the TradeLog to quantstats. crucible stops at the edge.

Go deeper

📖 The tutorialFrom Trade Log to Verdict: the statistics of a significant edge. Every technique worked end to end on a Donchian breakout, with references to the source literature. Prefer offline? Download it as a PDF.

Ecosystem

The only input crucible needs is a trade log. Build one with the bundled barrier simulator, export it from your backtester, or map any frame onto TradeLog — crucible hands back the verdict either way, whatever your data source.

If you also need futures / COT data to build a signal from, there's an optional companion:

  • cotdata — an optional data layer: a local, file-based store that wraps Norgate, Databento, or yfinance behind one read API, extensible to other data providers.

The flow runs one direction: cotdata (data) → your signal → crucible (edge). Neither imports the other — crucible works alone with any source of trades.


Open source under the MIT license · Changelog · PyPI