Spaces:
Running on Zero
A newer version of the Gradio SDK is available: 6.25.0
title: Bit Backtest Lab
emoji: π
colorFrom: yellow
colorTo: gray
sdk: gradio
sdk_version: 5.49.1
app_file: app.py
pinned: true
license: apache-2.0
hf_oauth: true
hf_oauth_scopes:
- inference-api
short_description: Compare forecasting models under honest costs
tags:
- finance
- backtesting
- time-series
- forecasting
Backtest Lab
The Bit Trading Company Β· backtest and compare forecasting models and rule-based trading strategies against a shared, precomputed signal store.
- App: https://huggingface.co/spaces/Bit-Trading-Company/bit-backtest-lab
- Signal store: https://huggingface.co/datasets/The-Bit-Trading-Company/bit-signal-store
What it is
Most backtesting tools make it easy to produce a beautiful, false result. This one is built around the handful of things that actually decide whether a backtest means anything: when you are allowed to trade, what it costs, and whether you ever tested on data you had not already fitted.
The store holds raw model outputs and prices only β never trade decisions. Trading rules, costs and sizing are applied live, per request, so many strategies can be compared over the same forecasts without re-running inference.
The Compare tab
The app opens on Compare, a view over everything this Space has ever computed β currently 168 combinations of strategy Γ model Γ asset Γ timeframe, precomputed under one canonical config so the rows are actually comparable with each other.
- Leaderboard β ranked board with filters, plus returns over time overlaying the top algorithms on one axis, and a risk/return scatter where marker area is trade count.
- Models β directional accuracy against naive baselines, band calibration, and best result per model.
- Signals β a signal aggregator: every model's latest stored forecast for one asset, weighted by how accurate that model has actually been on that slice, plus a consensus verdict.
- Run history β session runs merged with runs saved to the store, so a result found on one machine is visible from another.
Two editorial rules run through it. Rows with fewer than 20 trades are flagged and kept out of the default ranking, because a Sharpe of 4 on 13 trades will otherwise own the board forever. And naive baselines are ranked alongside the learned models everywhere β which is how you find out that on daily bars the drift baseline currently calls direction more often correctly than any Chronos variant. The Models tab says so in plain language.
Architecture
ββββββββββββββββββββββββββββββββββββββββββββββββ
β bit-backtest-lab (Gradio Space) β
β β
β Strategy Builder ββ β
β βββΊ engine.py (vectorbt) β
β Results Canvas ββββ next-bar-open fills β
β costs, stops, sizing β
β walk-forward + holdoutβ
β β β
β β read-only βββββββββββββββ β
β βββββββββββββββββββββΊβ LRU cache β β
β ββββββββ¬βββββββ β
β "Extend coverage" β β
β βββΊ @spaces.GPU (user's quota) β β
β βββΊ CommitScheduler βββ β β
ββββββββββββββββββββββββββββββββΌβββββββΌβββββββββ
βΌ β
ββββββββββββββββββββββββββββββββββββββββ
β bit-signal-store (Dataset) β
β manifest.json coverage map β
β signals/ raw q10/q50/q90 β
β prices/ OHLCV cache + source β
β comparisons/ precomputed tables β
β runs/ saved run summaries β
ββββββββββββββββββββββββββββββββββββββββ
β²
β batch refresh only
ββββββββββββββ΄ββββββββββββββββββββββββββ
β ccxt (BinanceβCoinbase) β
β yfinance β Stooq β Tiingo β
ββββββββββββββββββββββββββββββββββββββββ
How results are computed
Fills. Every fill executes at the next bar's open. A decision made at
bar t is shifted forward one bar by the engine and filled at open[t+1].
There is no configuration that enters on the signal bar; run_backtest applies
the shift itself and a strategy cannot bypass it.
No lookahead. Strategies are checked by perturbation, not by convention:
the tail of the price series is scaled up and down, the indicator is re-run, and
every output before the perturbation point must be bit-identical. A strategy
that reads bar t+1 β or normalises by a full-sample statistic, or uses a
centred rolling window β changes its earlier outputs and is caught.
Costs. On by default: commission per side plus a slippage model
(fixed bps or volume-scaled). Slippage is embedded in the fill price, so it is
reconstructed from the unslipped reference price and booked onto the trade row.
gross β costs = net holds exactly on every trade. Turning costs off is
possible and is labelled, loudly, as not real.
Validation. Simple split, walk-forward (configurable train/test/roll), and a
locked last-N-months holdout. Train and test never share a bar. The holdout is
excluded from selectable_index(), which is the only index any
parameter-selection path is given β it cannot be fitted on by accident. When a
configuration produces no out-of-sample period, the app says so instead of
printing 0.00.
Metrics. Total return, CAGR, Sharpe, Sortino, max drawdown, win rate,
profit factor, exposure and trade count, each computed in-sample,
out-of-sample, and on the holdout. Every displayed number comes from
metrics.py operating on the equity curve or the trade list.
Honest limitations
- Coverage is sparse. The seed covers 6 assets Γ 3 timeframes with eight models (Chronos-Bolt tiny/mini/small/base, Chronos-T5 small, and three naive baselines) β 78 signal slices, ~265k rows. Anything else needs extending.
- The learned models are not clearly beating the naive baselines on directional accuracy at daily resolution. That is a real result, and it is displayed rather than buried.
- Binance is geo-blocked from the seeding machine, so crypto prices came from Coinbase. Prices will not tick-match another venue.
- Equity intraday history is provider-capped β roughly 730 days of hourly and 60 days of 15-minute bars. This is recorded in the manifest as a coverage boundary, not hidden and not reported as an error.
- Stooq began serving an HTML block page instead of CSV during the build, so the equity fallback chain effectively runs on yfinance alone right now.
- Sentiment is a stub.
Sentiment-Gated Momentumruns against a labelled, deterministic price-derived proxy, not news sentiment. It sits behind aSentimentSourceinterface for a real feed later. - Pairs Trading and Custom (code) appear in the design's preset list but are not runnable. Custom-code strategies are disabled deliberately: this Space never executes user-supplied code.
- A backtest is a hypothesis. Survivorship, regime change, liquidity and your own future behaviour are not modelled.
Extending coverage
Anonymous visitors get full read and backtest access. Extending coverage
requires signing in with Hugging Face, because inference runs inside a
@spaces.GPU function on your ZeroGPU quota:
- Pick a model, asset, timeframe and range in the Coverage tab.
- Estimate shows the step count; the request is deduplicated against
manifest.json, so an already-covered range recomputes nothing. - Per-request caps apply (2 years daily / 6 months hourly / 2 months 15m).
- On success the new slice, the manifest, and the regenerated comparison tables are written back in a single atomic commit, attributed to you.
Add model takes an adapter family plus a Hub model id, resolves and pins the
revision, and runs a 100-step smoke test on your quota before the model appears
in the coverage map. Only allow-listed adapter families (chronos, timesfm)
can be constructed, and model ids are validated before they reach the Hub.
If your quota is exhausted the app says so and offers a duplicate-this-Space link β the store is public, so a duplicate reads the same data.
Running locally
uv venv --python 3.10 && uv pip install -r requirements.txt
python app.py
Seeding your own store:
python scripts/seed_store.py --plan v1 --dry-run
python scripts/seed_store.py --plan smoke --push
Tests:
bash tests/run_all.sh
HF_WRITE_TOKEN is required only for write-back; reading and backtesting work
without any token. See .env.example.
Disclaimer
Backtested results are hypothetical, derived from historical data, and are not indicative of future results. Nothing here is investment advice. The Bit Trading Company is not a licensed investment adviser.