MYTHOSLIVE / models /base_model.py
Kashaf1's picture
Upload rinking_out files
d9b9067
Raw History Blame Contribute Delete
4.58 kB
import numpy as np
import pandas as pd
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Union
def validate_history_and_horizon(history, horizon: int, model_name: str) -> None:
if horizon < 1:
raise ValueError(f'{model_name}: horizon must be >= 1, got {horizon}.')
values = np.asarray(pd.Series(history).astype(float), dtype=float)
if not np.all(np.isfinite(values)):
raise ValueError(f'{model_name}: price history contains NaN/inf. This usually means a data glitch left a missing or malformed candle -- re-fetch the history and try again.')
@dataclass
class Signal:
"""Unified prediction contract shared by every model in the project.
This is the single interface the backtester and the live engine consume, so
that price-forecasting models (ARIMA, Auto-ARIMA, ARIMA-GARCH, Moirai,
TimesFM) and the MAYTHOS decision engine are driven through exactly one code
path. See MERGE_NOTES.md (Decision 1).
Attributes:
direction: 'up' or 'down' -- the predicted move of the next close.
confidence: model confidence in [0, 1].
predicted_close: the forecast price for the final horizon step, or None
for models (like MAYTHOS) that emit a directional decision rather
than a price level.
horizon: number of steps ahead this signal describes.
meta: model-specific diagnostic fields (market state, execution
suitability, etc.). Empty for plain forecasters.
"""
direction: str
confidence: float
predicted_close: Optional[float] = None
horizon: int = 1
meta: Dict[str, Any] = field(default_factory=dict)
class BaseModel(ABC):
"""Root of the model hierarchy. Every model produces a :class:`Signal`."""
name = 'base'
@abstractmethod
def predict_signal(self, window: pd.DataFrame, horizon: int = 1, features: pd.DataFrame = None) -> Signal:
raise NotImplementedError
def _confidence_from_forecast(history: np.ndarray, predicted_close: float) -> float:
"""Map a numeric forecast to a [0, 1] confidence for the unified Signal.
The size of the predicted move is compared against the recent realised
volatility (standard deviation of first differences). A move of two standard
deviations or more saturates confidence at 1.0. This is a documented,
deterministic heuristic so that forecast-only models still populate the same
confidence field MAYTHOS reports natively. See MERGE_NOTES.md (Decision 1).
"""
arr = np.asarray(history, dtype=float)
current = float(arr[-1])
move = abs(float(predicted_close) - current)
diffs = np.diff(arr)
scale = float(np.std(diffs)) if diffs.size else 0.0
if scale <= 0.0:
return 1.0 if move > 0.0 else 0.0
return float(min(1.0, move / (2.0 * scale)))
class BaseForecastModel(BaseModel):
"""Base class for models that forecast a price path.
Subclasses implement :meth:`predict` (a list of future closes). The concrete
:meth:`predict_signal` here turns that path into the unified Signal, so every
forecaster automatically speaks the same contract as MAYTHOS without any
adapter or bridge layer.
"""
name = 'base'
@abstractmethod
def predict(self, history: pd.Series, horizon: int = 1, features: pd.DataFrame = None) -> list:
raise NotImplementedError
@staticmethod
def _history_series(window: Union[pd.DataFrame, pd.Series, Any]) -> pd.Series:
if isinstance(window, pd.DataFrame):
if 'Close' not in window.columns:
raise ValueError("forecast window DataFrame must contain a 'Close' column.")
return window['Close']
return pd.Series(window)
def predict_signal(self, window: pd.DataFrame, horizon: int = 1, features: pd.DataFrame = None) -> Signal:
history = self._history_series(window)
forecast: List[float] = list(self.predict(history, horizon=horizon, features=features))
if len(forecast) < 1:
raise ValueError(f'{self.name}: predict() returned an empty forecast.')
predicted_close = float(forecast[-1])
current_close = float(pd.Series(history).astype(float).iloc[-1])
direction = 'up' if predicted_close > current_close else 'down'
confidence = _confidence_from_forecast(pd.Series(history).astype(float).to_numpy(), predicted_close)
return Signal(direction=direction, confidence=confidence, predicted_close=predicted_close, horizon=horizon, meta={})