Files

51 lines
1.6 KiB
Python
Raw Permalink Normal View History

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