Metadata-Version: 2.4
Name: backtest-bias
Version: 0.2.0
Summary: Checks whether your backtest data is lying to you: survivorship, dead-name coverage, integrity gates for financial price panels.
Author: Ayan Jain
License: MIT
Project-URL: Homepage, https://financebroski.com
Project-URL: Repository, https://github.com/Finance-broski/backtest-bias
Project-URL: Bias Table, https://github.com/Finance-broski/backtest-bias/blob/main/BIAS_TABLE.md
Keywords: backtesting,survivorship-bias,quant,data-quality,point-in-time
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Office/Business :: Financial :: Investment
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pandas>=1.5
Requires-Dist: numpy>=1.23
Dynamic: license-file

# backtest-bias

[![DOI](https://zenodo.org/badge/1294836988.svg)](https://doi.org/10.5281/zenodo.21770386)

**Checks whether your backtest data is lying to you.**

Most backtests don't fail loudly. They flatter you quietly, because the data underneath them is
missing the stocks that died. This library tests your price panel for that, in one line, and
tells you roughly what it costs when it finds it.

## The measured numbers this library is built on

These are not estimates. I measured them on real Indian market data and published the write-ups:

- **24%** of the top-500 Indian stocks (as of 2015) are invisible to yfinance today: delisted,
  merged, or renamed with no public mapping. A universe built by fetching current listings runs
  on survivors only.
- Survivor-only universes inflated equal-weight returns by **+0.8 to +3.2 pp/yr** depending on
  the universe vintage and the survivor definition. Same market, same method, a factor of four
  apart. Anyone quoting one number is guessing. (Working paper under review at SSRN.)
- On the most widely used Kaggle NSE dataset, index-membership look-ahead added **+10%** terminal
  wealth cap-weighted and **+43%** equal-weighted over 2010-2021. The bias depends on construction.
- How much of a universe *should* be dead? Measured across six top-500 vintages (2012-2022,
  Indian equities), the curve is stable: **~5-8% by 3 years, 11-14% by 5, 17-21% by 7, 24-30% by
  10.** Verdicts quote the range matched to your window length. If your panel lost zero names,
  your panel is the problem.

## Citing this

If you use the library or the measured constants in [BIAS_TABLE.md](BIAS_TABLE.md), cite it as:

> Jain, A. (2026). *backtest-bias: survivorship and integrity checks for financial price panels*.
> Zenodo. https://doi.org/10.5281/zenodo.21770386

That DOI is the concept DOI: it always resolves to the latest version. GitHub also reads
`CITATION.cff` in this repo, so the "Cite this repository" button in the sidebar produces BibTeX
and APA directly.

## Install

```
pip install backtest-bias
```

## 30 seconds to a verdict

```python
import pandas as pd
from backtest_bias import check_survivorship

prices = pd.read_csv("my_panel.csv")   # wide (date x symbols) or long (date/symbol/close)
report = check_survivorship(prices)
print(report.summary())
```

```
survivorship check: 412 symbols over 9.2y, 0 died in-window (0%)
verdict: SEVERE - 412 names over 9.2y with zero deaths is the survivor-only signature;
comparable universes lose 22%-28% of names over 9y (measured)
expect EW returns inflated roughly +0.8-3.2 pp/yr vs an honest universe (measured,
vintage-dependent; see backtest_bias.REFERENCES)
```

## What v0.2 adds

```python
from backtest_bias import check_identity, check_universe

# is each ticker the same company all the way through its series?
print(check_identity(prices).summary())

# was your member list knowable on the backtest's start date, and does it die like a real one?
print(check_universe(prices, universe=my_symbols, start="2015-01-01").summary())
```

| call | question it answers |
|---|---|
| `check_identity(prices)` | recycled tickers: a dead company's history silently stitched to a new listing on the same symbol. Four such tickers moved a measured US result by 1.7 pp/yr, more than the survivorship bias itself |
| `check_universe(prices, universe, start)` | the two signatures of today's list applied backwards: members whose data begins after the start date, and a universe whose start-alive names almost never die (measured death curves say they should) |

`REFERENCES` now carries the measured US constants alongside the Indian ones: survivor-filter effect +0.4 to +1.0 pp/yr by vintage, yearly gaps swinging -7.5 to +3.5, and the 1.7 pp/yr identity error.

## What v0.1 ships

| function | what it answers |
|---|---|
| `check_survivorship(prices)` | does my universe contain the stocks that died, or only the winners? Full report with severity and a measured bias estimate |
| `dead_name_ratio(prices)` | one number: what fraction of my names end before the panel does. `0.0` = pure survivor panel |
| `assert_integrity(prices)` | CI gate: raise if the panel smells survivor-only, so a silent re-download of bad data fails your pipeline instead of flattering your backtest |

Input handling is forgiving: wide panels, long frames, sniffed column names, NaN-padded
histories. Anything the library cannot judge honestly, it raises instead of guessing.

## Roadmap

- **v0.2**: look-ahead / point-in-time violations: fundamentals dated by period instead of
  announcement, index membership applied backwards, same-bar signal fills
- **v0.3**: rename-continuity and corporate-action gap detection

## Who, and how to get this run on your own data

I'm [Ayan Jain](https://www.linkedin.com/in/ayanjain259). I build point-in-time Indian equity
data and audit backtests and datasets for the biases that inflate them. The measured numbers
above come from those audits.

If you want this class of check run on your own backtest or dataset by a person instead of a
library, that's my **Bias Check**: you send the backtest or data, and within 48 hours you get a
written verdict on survivorship, look-ahead / point-in-time integrity, cost realism, and marking,
with what's wrong and roughly what it costs in return terms. Fixed price, Rs 7,500. Start it
through the [intake form](https://forms.gle/sAvosfHnitCBm9FD7) or email ayanjain259@gmail.com.
Larger or ongoing work is scoped separately, tell me the problem and I'll send a quote.

**Public data and replication:** a survivorship-free Indian equity dataset (NSE/BSE) and a
runnable notebook that visualizes the bias on a sample are on Kaggle under
[financebroski](https://www.kaggle.com/financebroski) (the
[dataset](https://www.kaggle.com/datasets/financebroski/survivorship-free-indian-equity-data-nsebse)
and the [notebook](https://www.kaggle.com/code/financebroski/survivorship-bias-visualized-indian-equity-sample)).

**The Bias Table:** every measured number behind this library, one page, citable:
[BIAS_TABLE.md](BIAS_TABLE.md). New rows land by email at
[The Bias Ledger](https://financebroski.substack.com).

MIT licensed. Issues and war stories welcome, especially datasets that fooled you.

## The practice behind this

I audit backtests and datasets professionally. The public record, 155 strategies tested and 143 killed, lives at [financebroski.com/graveyard.html](https://financebroski.com/graveyard.html); the audit practice is at [financebroski.com](https://financebroski.com).
