Composable models and A/B tests of the 2024 ideas; add powerlaw.

Models are now a Composite of drift, volatility and (optional) shape
components, so an experiment can swap one part against a fixed control.

btcmodel/experiments.py holds seven experiments built from the ideas in the
old branches (catalogued in docs/2024-ideas.md), each with its hypothesis
and source, and a verdict rule fixed before anything ran. `just ab` runs
them on development data. Results:

- Shrinking the cycle drift, and a power-law trend (plain or reverting),
  beat their controls. The power law beats the random walk by 53-63% at
  3-4 years with unbiased outcomes, so it is promoted to MODELS.
- Every alternative volatility estimate (EWMA blends, other windows,
  reversion to a level or trend) is worse than the trailing 365-day window.
  Cycle-dependent volatility, heavy tails and stretched cycle phase show no
  reliable effect.
This commit is contained in:
sam
2026-09-24 03:01:46 -07:00
parent cfc27a38de
commit b0243adf61
18 changed files with 795 additions and 155 deletions
+50
View File
@@ -0,0 +1,50 @@
"""
Models built from parts.
Most ideas about Bitcoin prices say something about either the expected return
(drift) or the size of the uncertainty (volatility). A Composite pairs one of
each, so an A/B test can swap exactly one part and hold the other fixed.
"""
from dataclasses import dataclass
from typing import Protocol
import numpy as np
import pandas as pd
from ..forecast import Forecast
from .shape import Normal
class Drift(Protocol):
def expected_log_return(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
"""Expected cumulative log return from the origin to each horizon."""
...
class Volatility(Protocol):
def sd(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
"""Standard deviation of the cumulative log return at each horizon."""
...
class Shape(Protocol):
def standard_quantiles(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
"""Quantiles at LEVELS of a mean-0, sd-1 distribution, shape (H, N_LEVELS)."""
...
@dataclass(frozen=True)
class Composite:
"""Log price: mean from `drift`, spread from `volatility`, normal unless `shape` says."""
name: str
drift: Drift
volatility: Volatility
shape: Shape = Normal()
def forecast(self, history: pd.DataFrame, horizons: np.ndarray) -> Forecast:
mean = np.log(history["close"].iloc[-1]) + self.drift.expected_log_return(history, horizons)
sd = self.volatility.sd(history, horizons)
z = self.shape.standard_quantiles(history, horizons)
return Forecast(history.index[-1], horizons, mean[:, None] + sd[:, None] * z)