Files
bitcoin-model/btcmodel/models/shape.py
T

53 lines
1.8 KiB
Python
Raw Normal View History

"""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