File size: 9,748 Bytes
6dbc1b3
 
46f1a78
6dbc1b3
46f1a78
6dbc1b3
46f1a78
6dbc1b3
46f1a78
 
 
 
 
fbfed0d
46f1a78
 
 
 
 
6dbc1b3
 
46f1a78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fbfed0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46f1a78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fbfed0d
 
 
 
 
 
46f1a78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1139375
46f1a78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
---
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 Momentum` runs against a labelled,
  deterministic price-derived proxy, *not* news sentiment. It sits behind a
  `SentimentSource` interface 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:

1. Pick a model, asset, timeframe and range in the **Coverage** tab.
2. **Estimate** shows the step count; the request is deduplicated against
   `manifest.json`, so an already-covered range recomputes nothing.
3. Per-request caps apply (2 years daily / 6 months hourly / 2 months 15m).
4. 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

```bash
uv venv --python 3.10 && uv pip install -r requirements.txt
python app.py
```

Seeding your own store:

```bash
python scripts/seed_store.py --plan v1 --dry-run
python scripts/seed_store.py --plan smoke --push
```

Tests:

```bash
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.