Skip to main content

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? leakage — check_prefix_invariance, purged_splits
Luck Is the "edge" better than a skill-free null? placebo — ar1_matched_placebo, compare_against_placebos
Selection Did I overfit by trying many configs? multiplicity — deflated_sharpe, pbo, TrialRegistry
Fragile inference Is my ΔSharpe / forecast edge significant? metrics, forecast_eval — sharpe_difference_test, dm_test, mincer_zarnowitz
Regime concentration Does it survive out of one lucky regime? robustness — subperiod_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.

Metadata

Release files for nullius 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nullius 0.1.0
File Size Uploaded
nullius-0.1.0.tar.gz 297.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nullius 0.1.0
File Interpreter ABI Platform
nullius-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 352.1 kB

Release files / nullius-0.1.0.tar.gz

Download URL nullius-0.1.0.tar.gz
Size 297.3 kB
Tags Source
SHA-256 checksum
How to use checksums
5169e0916d962ed7c02012fc1f59870ab2a46430ad01509d93adbc23462e790c
BLAKE2b-256 checksum
How to use checksums
75123082e99a34dcb4130c68b56e4ae2fb8a56d9139ae0db2be8f15fee7004e9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.6

Release files / nullius-0.1.0-py3-none-any.whl

Download URL nullius-0.1.0-py3-none-any.whl
Size 54.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bae2f7b84ce721a19cebdb285dd7e2a7c7b0876aa67cd3f874f8572a51e29204
BLAKE2b-256 checksum
How to use checksums
5ec49437998ac73e05c4bb62f9523ea949bad8e7f800f7870cabd6f5445cad72
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.6

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

0.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page