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,52 @@
|
||||
"""Distribution shapes: standardised quantiles (mean 0, sd 1) at each horizon."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from scipy.stats import norm, t
|
||||
|
||||
from ..forecast import LEVELS
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Normal:
|
||||
def standard_quantiles(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||
return np.broadcast_to(norm.ppf(LEVELS), (len(horizons), len(LEVELS)))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StudentT:
|
||||
"""Student's t with `df` degrees of freedom, rescaled to unit variance."""
|
||||
|
||||
df: float = 4.0
|
||||
|
||||
def standard_quantiles(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||
q = t.ppf(LEVELS, self.df) / np.sqrt(self.df / (self.df - 2))
|
||||
return np.broadcast_to(q, (len(horizons), len(LEVELS)))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Empirical:
|
||||
"""
|
||||
Filtered historical simulation at the horizon level: the shape of past
|
||||
h-day log returns, each divided by the trailing volatility at its start.
|
||||
|
||||
Overlapping h-day returns are far from independent, so a horizon falls back
|
||||
to normal unless the history spans at least `min_windows` of them.
|
||||
"""
|
||||
|
||||
vol_window: int = 365
|
||||
min_windows: int = 3
|
||||
|
||||
def standard_quantiles(self, history: pd.DataFrame, horizons: np.ndarray) -> np.ndarray:
|
||||
log_price = np.log(history["close"])
|
||||
sigma = log_price.diff().rolling(self.vol_window).std()
|
||||
out = np.empty((len(horizons), len(LEVELS)))
|
||||
for i, h in enumerate(horizons):
|
||||
z = ((log_price.shift(-h) - log_price) / (sigma * np.sqrt(h))).dropna()
|
||||
if len(z) < self.min_windows * h:
|
||||
out[i] = norm.ppf(LEVELS)
|
||||
else:
|
||||
out[i] = np.quantile((z - z.mean()) / z.std(), LEVELS)
|
||||
return out
|
||||
Reference in New Issue
Block a user