Skip to main content

Falsify-first backtest diagnostics: lookahead detection with negative controls, skill placebos, honest forecast evaluation, and multiple-testing deflation.

Project description

Nullius

Nullius

A falsify-first toolkit for backtests: lookahead detection with negative controls,
skill placebos, honest forecast evaluation, and multiple-testing deflation.


Every backtesting library — backtrader, vectorbt, zipline, bt — helps you produce a performance number. Almost nothing helps you attack one. nullius is the missing "falsify-first" layer: the methodological weapons professionals use to prove a result is real — purged cross-validation, prefix-invariance leak detection, AR(1)-matched placebos, Deflated Sharpe, Probability of Backtest Overfitting — packaged, tested, and documented so a retail quant can pip install them and answer: is this Sharpe 1.2 real, or did I manufacture it?

The war story

A real lookahead bug once survived four weeks inside a careful research process. A single global statistic — a full-sample quantile — was computed once and used inside a per-timestamp decision. It looked innocent. It inflated a Calmar ratio by ~30%. What finally killed it was not a sharper eye; it was a regression test with a negative control — a test that first proved it could detect the leak by firing on a deliberately broken pipeline, then proved the fix by staying silent on the corrected one.

That is the philosophy this library is built on, stated verbatim in its tests:

A leak detector you have never seen fire is itself untested.

Every probe in nullius ships with a negative control that must fire in CI.

30-second quickstart

import numpy as np, pandas as pd
import nullius as nl

# a pure-noise prediction panel — there is no signal here
t = pd.date_range("2020-01-01", periods=250, freq="B")
data = pd.DataFrame({"time": np.repeat(t, 3),
                     "asset": ["SPY", "QQQ", "IWM"] * len(t),
                     "predicted": np.random.default_rng(0).normal(size=3 * len(t))})

# leaky_global_quantile_gate thresholds on the FULL-SAMPLE quantile — a lookahead
leak = nl.check_prefix_invariance(nl.leaky_global_quantile_gate, data)
print(leak.passed, leak.severity)     # False fatal  — decisions dated <= T changed
print(leak.evidence.head())           # the exact rows that moved

# the expanding-quantile version uses only past data and passes
ok = nl.check_prefix_invariance(nl.clean_expanding_quantile_gate, data)
print(ok.passed)                      # True

What this is / is not

  • It is a set of checks. Each returns a CheckResult — a verdict, a statistic, a threshold, and the evidence rows — and many together render as a Markdown audit document. The output is not a float; it is a document you are meant to read.
  • It is not a backtester. It does not produce signals, place trades, or fetch data. It audits the statistics of a backtest you already have. All examples run on synthetic data generated in-repo.

The five failure modes

Failure mode Question it answers Module
Leak Did a decision use future data? leakagecheck_prefix_invariance, purged_splits
Luck Is the "edge" better than a skill-free null? placeboar1_matched_placebo, compare_against_placebos
Selection Did I overfit by trying many configs? multiplicitydeflated_sharpe, pbo, TrialRegistry
Fragile inference Is my ΔSharpe / forecast edge significant? metrics, forecast_evalsharpe_difference_test, dm_test, mincer_zarnowitz
Regime concentration Does it survive out of one lucky regime? robustnesssubperiod_ic_stability, regime_stratified_metrics

Any timeframe

The general core is frequency-blind: it operates on observation counts, not the calendar, so DSR, PBO, prefix-invariance, QLIKE/MZ/DM, and the equity metrics are correct at any fixed-bar frequency. Every function that needs time-scaling takes an optional clock: Clock; the DAILY preset supplies the 252 / √252 defaults, and any other frequency is one Clock(periods_per_year=...) away. The frequency-sensitive checks (session-aware purging, intraday seasonal placebos, microstructure-noise-robust estimators) are planned for the optional nullius[intraday] extra.

Install

pip install nullius                 # core: numpy, pandas, scipy
pip install "nullius[reference]"    # + statsmodels, only for cross-check tests

Requires Python ≥ 3.11. Core runtime depends only on numpy / pandas / scipy — Mincer-Zarnowitz and rolling calibration are implemented in numpy (their HAC covariance reproduces statsmodels to ~1e-12; the optional [reference] extra runs that cross-check).

Examples

Runnable, synthetic-data-only, each under 100 lines:

Citations

Patton (2011, J. Econometrics) — QLIKE robust loss · Diebold & Mariano (1995) + Harvey, Leybourne & Newbold (1997) — equal-predictive-accuracy test + small-sample correction · Mincer & Zarnowitz (1969) — forecast efficiency · Bailey & López de Prado (2014, JPM) — Deflated Sharpe · Bailey, Borwein, López de Prado & Zhu (2017) — Probability of Backtest Overfitting (CSCV) · Jobson & Korkie (1981) + Memmel (2003) — Sharpe-difference test · Lo (2002) — Sharpe standard error · Politis & Romano (1994) — stationary bootstrap · López de Prado (2018, Advances in Financial ML) — purging & embargo.

License

MIT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nullius-0.1.0.tar.gz (297.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nullius-0.1.0-py3-none-any.whl (54.9 kB view details)

Uploaded Python 3

File details

Details for the file nullius-0.1.0.tar.gz.

File metadata

  • Download URL: nullius-0.1.0.tar.gz
  • Upload date:
  • Size: 297.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.6

File hashes

Hashes for nullius-0.1.0.tar.gz
Algorithm Hash digest
SHA256 5169e0916d962ed7c02012fc1f59870ab2a46430ad01509d93adbc23462e790c
MD5 24dd93f457500600e512c64cb93073c0
BLAKE2b-256 75123082e99a34dcb4130c68b56e4ae2fb8a56d9139ae0db2be8f15fee7004e9

See more details on using hashes here.

File details

Details for the file nullius-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: nullius-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 54.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.6

File hashes

Hashes for nullius-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bae2f7b84ce721a19cebdb285dd7e2a7c7b0876aa67cd3f874f8572a51e29204
MD5 2ffa520161a54ab49ecd6601629a6f15
BLAKE2b-256 5ec49437998ac73e05c4bb62f9523ea949bad8e7f800f7870cabd6f5445cad72

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page