| """ |
| Common interface every forecasting model implements, so the rest of the app |
| (backtester, Gradio UI) can treat ARIMA / Auto-ARIMA / ARIMA-GARCH / Moirai / |
| TimesFM interchangeably without knowing their internal details. |
| """ |
| from abc import ABC, abstractmethod |
|
|
| import pandas as pd |
|
|
|
|
| class BaseForecastModel(ABC): |
| name = "base" |
|
|
| @abstractmethod |
| def predict(self, history: pd.Series, horizon: int = 1, features: pd.DataFrame = None) -> list: |
| """ |
| Given a 1-D series of past closing prices (oldest -> newest), return |
| a list of `horizon` forecasted future closing prices. |
| |
| `features`, if given, is a DataFrame of extra covariates (OHLCV + |
| technical indicators β see features/feature_pipeline.py's |
| FEATURE_COLUMNS) covering the SAME candles as `history`, in the SAME |
| order, one row per candle (len(features) == len(history)). Every |
| value in it was already known as of its own candle β callers (the |
| backtester, the UI, live_runner.py) are responsible for never |
| building a `features` window that reaches past the last candle in |
| `history`; see features/feature_pipeline.py for how that's kept |
| leak-free, and its carry_forward_future() for how each model asks |
| for covariate values over the FORECAST horizon (where real future |
| values don't exist yet). |
| |
| `features=None` (the default) means Close-price-only β every |
| model's original behavior, completely unchanged. |
| |
| Implementations should raise a clear, specific exception (ImportError |
| for a missing dependency, ValueError for bad input) rather than |
| silently returning a wrong/empty result β the backtester and UI both |
| catch and display these per-model, without crashing other models. |
| """ |
| raise NotImplementedError |
|
|
| def is_available(self) -> bool: |
| """True if this model's dependencies are installed and importable.""" |
| try: |
| self._check_dependencies() |
| return True |
| except Exception: |
| return False |
|
|
| def _check_dependencies(self): |
| """Override to raise ImportError if a required package is missing.""" |
| return None |
|
|
| def describe(self) -> dict: |
| """Model identity metadata for logging/reproducibility (P0 item: |
| "log exact model identity β model name, checkpoint, package |
| version, backend, device, context length, normalization, seed, |
| preprocessing version"). Returns a flat dict; a field this model |
| doesn't have (e.g. "checkpoint" for ARIMA, which has none) is |
| simply omitted rather than filled with a placeholder. Override in |
| subclasses to add model-specific fields β always start from |
| `super().describe()` and update it, so every model at minimum |
| reports its name and this project's feature/preprocessing version |
| (features/feature_pipeline.py's FEATURE_PIPELINE_VERSION), which |
| matters here specifically because a fitted (p,d,q) order or a |
| cached foundation-model instance is only valid for the exact |
| feature definitions it was produced under β see |
| models/registry.py's cache-key docstring. |
| """ |
| from features.feature_pipeline import FEATURE_PIPELINE_VERSION |
| return { |
| "model": self.name, |
| "class": type(self).__name__, |
| "preprocessing_version": FEATURE_PIPELINE_VERSION, |
| } |
|
|