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:
@@ -0,0 +1,44 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from scipy.stats import norm
|
||||
|
||||
from btcmodel.forecast import Forecast, crps, pit
|
||||
|
||||
|
||||
def normal_crps(mu, sigma, y):
|
||||
"""Closed-form CRPS of N(mu, sigma^2) at y."""
|
||||
z = (y - mu) / sigma
|
||||
return sigma * (z * (2 * norm.cdf(z) - 1) + 2 * norm.pdf(z) - 1 / np.sqrt(np.pi))
|
||||
|
||||
|
||||
def test_crps_matches_closed_form_for_normal():
|
||||
f = Forecast.normal("2020-01-01", np.array([1, 2, 3]), mean=[0.0, 1.0, 2.0], sd=[1.0, 0.5, 2.0])
|
||||
y = np.array([0.3, -0.2, 5.0])
|
||||
expected = normal_crps(np.array([0.0, 1.0, 2.0]), np.array([1.0, 0.5, 2.0]), y)
|
||||
np.testing.assert_allclose(crps(f.log_quantiles, y), expected, rtol=0.02)
|
||||
|
||||
|
||||
def test_crps_prefers_the_right_forecast():
|
||||
y = np.array([0.0])
|
||||
good = Forecast.normal("2020-01-01", np.array([1]), 0.0, 0.1)
|
||||
biased = Forecast.normal("2020-01-01", np.array([1]), 0.5, 0.1)
|
||||
vague = Forecast.normal("2020-01-01", np.array([1]), 0.0, 2.0)
|
||||
assert crps(good.log_quantiles, y) < crps(biased.log_quantiles, y)
|
||||
assert crps(good.log_quantiles, y) < crps(vague.log_quantiles, y)
|
||||
|
||||
|
||||
def test_pit_and_intervals():
|
||||
f = Forecast.normal("2020-01-01", np.array([1, 1, 1]), 0.0, 1.0)
|
||||
np.testing.assert_allclose(
|
||||
pit(f.log_quantiles, [0.0, 1.0, -10.0]), [0.5, 0.841, 0.0], atol=0.01
|
||||
)
|
||||
lo, hi = f.interval(0.95)
|
||||
np.testing.assert_allclose(hi, 1.96, atol=0.01)
|
||||
np.testing.assert_allclose(lo, -1.96, atol=0.01)
|
||||
|
||||
|
||||
def test_from_samples_recovers_quantiles():
|
||||
rng = np.random.default_rng(0)
|
||||
f = Forecast.from_samples("2020-01-01", np.array([10]), rng.normal(0, 1, size=(200_000, 1)))
|
||||
np.testing.assert_allclose(f.quantile(0.5), 0.0, atol=0.02)
|
||||
assert f.dates[0] == pd.Timestamp("2020-01-11")
|
||||
Reference in New Issue
Block a user