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:
@@ -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)
|
||||
Reference in New Issue
Block a user