Replace the 2024 model (model.py, ~2000 lines) with the btcmodel package, the baseline for future work: - Forecasts are quantiles of log price at each horizon, scored with CRPS in a walk-forward backtest (origins every 30 days from 2014, horizons 1 month to 4 years). Skill is relative to a zero-drift random walk, with circular block-bootstrap intervals and a count of independent windows. - Development data stops at 2024-11-26, the last day the 2024 model saw. Later outcomes are a holdout, scored only by `backtest --holdout`. - Models: random_walk, drift_rw, and cycle (the 2024 model's cycle-position drift, now kernel-smoothed and recency-weighted). On development data nothing beats the random walk with confidence; cycle loses at every horizon. - Prices: the Investing.com archive moves to data/ (cut at 2024-11-26; its last row was intraday) and is extended with Coinbase daily closes by `update`. Also: Nix flake dev shell (Python 3.13, pandas 3), ruff in place of black, pytest suite, and a rewritten README. NOTES.md is removed as inaccurate, and poetry is dropped.
4.7 KiB
Bitcoin Price Model
Don't take this seriously. It's all in good fun.
The 2024 edition
In November 2024 I asked Claude (3.6 Sonnet, via copy and paste in Claude Web) to help build a Bitcoin price model. After a lot of branching it produced ~2000 lines of cycle analysis, "market fundamentals", era adjustments and Monte Carlo, and forecasts like this one (as of 2024-11-14):
In September 2026 a newer Claude scored the forecasts it made on 2024-11-27 against what actually happened:
| 2024 forecast (median) | Actual | |
|---|---|---|
| Cycle top | 2025-10-21, $199K | 2025-10-06, $124.7K |
| 2026-06-30 | $133K (95% floor $65K) | $58.5K |
| 2026-09-23 | $124K | $84.4K |
It called the timing of the top within 15 days, 19 months ahead, but put the level ~60% too high. "The price stays at $92.7K" was a better forecast (18% mean error against 68%). Its "95%" intervals were really ~81% intervals past a year out, thanks to a fudge factor that narrowed them at long horizons.
That code lives in the jj/git history. This is the rewrite.
How it works now
A forecast is a probability distribution of the price at each horizon, not a line. Every model produces quantiles of log price, and every model is scored the same way:
- Walk-forward. Every 30 days from 2014 on, each model sees only the data up to that day and forecasts 1 month to 4 years ahead.
- CRPS. Each forecast is scored against what happened with the continuous ranked probability score, in log-price units (0.1 ≈ "typically 10% off"). It rewards being sharp and being calibrated at once, and can't be gamed by narrowing or widening intervals.
- Baselines. Skill is reported relative to a zero-drift random walk, with a
90% block-bootstrap interval. Nearby forecasts overlap heavily, so the report
also shows
windows: the number of genuinely independent outcomes. - Holdout. Development only sees data up to 2024-11-26, the last day the
2024 model saw. Everything after it is held out, and is scored only by
just holdout, once per round of model changes. (Caveat: we already know roughly what happened in 2025-26, so it isn't perfectly blind.)
Models
random_walk: zero drift; "it stays about here, give or take".drift_rw: drift equal to the last four years' average; "it keeps doing what it did last cycle".cycle: the 2024 model's one real idea. Expected return depends on the day of the halving cycle, estimated from past cycles, with recent cycles weighted more.
Findings so far (development data)
- Nothing beats the random walk with any confidence at any horizon. At 2-4 years there are only 3-5 independent outcomes in the whole history.
drift_rwleads at 2-4 years (+13-18% skill, but the intervals span zero).cycleloses to both at every horizon, and so does every setting tried (recency half-life 0.25-2 cycles, smoothing bandwidth 15-60 days). The cycle shape costs accuracy. The level is the problem: each cycle has grown less than the last (log return 4.0, 2.6, 2.0, i.e. roughly ×55, ×13, ×7), so any average of past cycles overshoots.
That points at diminishing returns as the structure worth modelling, e.g. a power-law trend, which is next.
Usage
With Nix:
nix develop
just update # fetch new daily prices from Coinbase
just backtest # score models on development data -> output/backtest/
just forecast # forecast from the latest price -> output/forecast/
just test
just holdout # score on held-out outcomes; sparingly
Without Nix, any Python 3.13 with numpy, pandas 3, scipy and matplotlib works:
python -m btcmodel --help.
Layout
btcmodel/
data.py price loading (Investing.com archive + Coinbase), dev cutoff
halving.py halving calendar, position in cycle
forecast.py Forecast (quantiles of log price), CRPS, PIT
evaluate.py walk-forward backtest and summary
models/ one file per model family; register new ones in __init__.py
plots.py fan chart, skill and calibration charts
A model is any object with a name and
forecast(history, horizons) -> Forecast. history is a date-indexed frame
holding everything up to the forecast origin and nothing after it. Other data
sources (hash rate, on-chain metrics, macro series) can join it as extra
columns. Anything published with a lag must be shifted to the date it was
actually available, or the backtest will quietly cheat.
Price data: Investing.com through 2024-11-26, then Coinbase Exchange daily closes (UTC).

