Rewrite as a probabilistic model with walk-forward evaluation.

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.
This commit is contained in:
sam
2026-09-24 02:19:02 -07:00
parent eefff47070
commit cfc27a38de
24 changed files with 1767 additions and 3124 deletions
+47
View File
@@ -0,0 +1,47 @@
"""Reference forecasts every other model has to beat."""
from dataclasses import dataclass
from typing import ClassVar
import numpy as np
import pandas as pd
from ..data import log_returns
from ..forecast import Forecast
@dataclass(frozen=True)
class RandomWalk:
"""
Zero-drift random walk in log price: "it stays about here, give or take".
Volatility is the trailing standard deviation of daily log returns.
"""
name: ClassVar[str] = "random_walk"
vol_window: int = 365
def forecast(self, history: pd.DataFrame, horizons: np.ndarray) -> Forecast:
sigma = log_returns(history).iloc[-self.vol_window :].std()
mean = np.log(history["close"].iloc[-1])
return Forecast.normal(history.index[-1], horizons, mean, sigma * np.sqrt(horizons))
@dataclass(frozen=True)
class DriftRandomWalk:
"""
Random walk whose drift is the mean daily log return over the trailing
`drift_window` days (one halving cycle by default): "it keeps doing what it
did last cycle".
"""
name: ClassVar[str] = "drift_rw"
drift_window: int = 1460
vol_window: int = 365
def forecast(self, history: pd.DataFrame, horizons: np.ndarray) -> Forecast:
returns = log_returns(history)
mu = returns.iloc[-self.drift_window :].mean()
sigma = returns.iloc[-self.vol_window :].std()
mean = np.log(history["close"].iloc[-1]) + mu * horizons
return Forecast.normal(history.index[-1], horizons, mean, sigma * np.sqrt(horizons))