honest-backtest
Your prediction-market backtest is lying to you. This one tells the truth.
Most backtests paper-fill at the displayed ask and report a win rate. On an
efficient venue the transactable price already contains the signal — so
WR ≈ ask by construction, and the "edge" you see is an artifact of filling at
prices you could never actually get. Deploy on that and you lose money the
backtest swore you'd make.
honest-backtest grades every decision against the price you could actually
transact at:
edge_real = mean(won − fill_px)— the only honest headline. Win rate alone is meaningless; if you pay the ask and the ask equals your win rate, your edge is zero.- Realizable fills, not the paper ask — two lenses:
- book-persistence: did the ask still rest at your limit ~1s later, or did it pull above it (the classic "asks vanish on the winners")?
- tape-corroboration: did a real trade print at your price? Proves the ask was genuine, not a thin phantom ladder.
- Real fees — PM taker fee is
fee_rate·p·(1−p)per share; makers pay 0. No hand-wavymin(p,1−p). - Adverse-selection diagnostic — a Fokker-Planck fair-value model flags trades where the book baited you (cheap ask) but the physics disagree: the fills you get are the losers; the winners reject you.
ghost_gap= paper edge − honest edge = the adverse-selection drag, in one number.
This is the harness that retrodicted six consecutive live strategy deaths on Polymarket up/down markets — every one looked great on paper and lost real money.
Install
pip install honest-backtest # numpy is the only dependency
Quick start
from honest_backtest import Signal, Decision, evaluate
class BuyTheCheapNo(Signal):
name, family, mode = "cheap_no", "demo", "taker"
def decide(self, ctx, i):
# read ONLY indices <= i (no lookahead). Return a Decision or None.
if ctx.book_ok(i) and 0 < ctx.na[i] < 0.40 and ctx.yes_mid(i) >= 0.56:
return Decision(i=i, ts_ms=int(ctx.ts[i]), token_yes=False,
action="taker", target_px=float(ctx.na[i]), size=20.0)
return None
row = evaluate(BuyTheCheapNo(), ctxs) # ctxs: iterable of SlotCtx
print(row["headline_edge_real"], row["ghost_gap"])
Bring your own SlotCtx stream (build them from your data with
SlotCtx.from_rows), or load the open Polymarket up/down dataset
straight from the Hub parquet (no 2 GB download):
pip install "honest-backtest[parquet]"
huggingface-cli download kinzikdza/polymarket-updown-microstructure \
--repo-type dataset --local-dir pm_data
from honest_backtest.adapters.parquet_pm import load_corpus
ctxs = load_corpus("pm_data/parquet", coins=("btc",), durations=("5m",))
A SQLite adapter (adapters.sqlite_pm.load_corpus, stdlib-only) is also there if
you have the data as a .sqlite file.
The calibration anchor
The shipped example no_overpriced is a calibration gate, not a strategy to
trade. Live it ran 325 fills at WR 0.382 vs avg ask 0.386 → edge_real = −0.004.
A naive paper backtest of the same rule showed +0.07. The honest harness must
reproduce ~0/negative here:
python -m honest_backtest.examples.no_overpriced pm_data/parquet # or open_dataset.sqlite
If it prints a strongly positive edge_real, the fill model has drifted
optimistic — that's a bug in fills.py, not an edge.
Why edge_real, not WR
WR > 50% with ask > WR is not an edge — the market priced higher than you win. Only WR > ask is a real edge.
On an efficient venue, corr(signal, outcome) is usually positive (momentum,
spot-vs-strike do predict) — but that's not the same as beating the
transactable price. The gap between the two is adverse selection, and it's
exactly what ghost_gap measures.
API
| function | what it does |
|---|---|
evaluate(signal, ctxs) |
run a signal over a corpus → one leaderboard row |
run_signal(signal, ctxs) |
→ raw per-decision grade dicts |
grade_taker(ctx, decision) |
book-persistence + tape-corroboration fill grade |
grade_maker(ctx, decision) |
queue-aware resting-bid fill grade |
leaderboard_row(name, family, mode, recs) |
aggregate grades → edge_real, CIs, $ROI, breakdowns |
physics_features(ctx, i, buy_yes) / adverse_score(feat, buy_yes) |
adverse-selection diagnostic |
won_buy, fee_per_share, fp_prob_yes, bootstrap_ci |
building blocks |
Signal, SlotCtx, SlotMeta, Decision are the data model. A Signal
implements decide(ctx, i) reading only indices <= i.
Dataset
The companion open dataset (6 weeks of real Polymarket BTC/ETH/SOL/XRP up/down microstructure — book snapshots, trade tape, and gamma-oracle resolutions) ships in the schema this library's SQLite adapter reads. See the dataset card for provenance and caveats.
License
Apache-2.0.
Metadata
Release files for honest-backtest 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| honest_backtest-0.2.0.tar.gz | 35.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| honest_backtest-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 61.7 kB
Release files / honest_backtest-0.2.0.tar.gz
| Download URL | honest_backtest-0.2.0.tar.gz |
|---|---|
| Size | 35.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4bc0a754202d4f0feb0b00d195e73da54cfaa1129f33f088d2de274349ac14d4
|
|
BLAKE2b-256 checksum How to use checksums |
2ca1bfae2dafff19682274e7ca6b9b1cee06454473a962c1fc8e9bd06c386449
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Release files / honest_backtest-0.2.0-py3-none-any.whl
| Download URL | honest_backtest-0.2.0-py3-none-any.whl |
|---|---|
| Size | 25.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5cc3c6dc9917692268824cf88f211bd250143145ad199032fa2280033db779be
|
|
BLAKE2b-256 checksum How to use checksums |
ea32a4786aeb41c6721958fc59e7c46b540785472ae8f5761c7d76570942f30c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|