Source code for numeraire.baselines.forecast

"""The historical-mean forecast baseline — the Goyal-Welch OOS reference.

The prevailing (expanding) historical mean of the returns block is the benchmark every predictive
regression is scored against: Goyal-Welch (2008) show it is a stubbornly hard forecast to beat out
of sample, and the OOS R^2 in :class:`~numeraire.core.evaluators.OutOfSampleR2Evaluator` measures
MSE
improvement *relative to it*. The walk-forward forecast engine already computes exactly this
benchmark column for free at each origin (``train.returns_frame().mean(axis=0)``); this estimator
exposes the very same quantity as a first-class ``to_forecast`` citizen, so it can be compared,
registered and run through the engine like any other method (its OOS R^2 against the engine
benchmark is ~0 by construction — it *is* the benchmark).

``window`` gives the rolling variant (last ``k`` observations); ``None`` (default) is the expanding
prevailing mean, matching the engine's benchmark convention.
"""

from __future__ import annotations

import pandas as pd

from numeraire.core import capabilities
from numeraire.core.data import TimeSeriesView
from numeraire.core.protocols import DataView


def _as_tsv(view: DataView) -> TimeSeriesView:
    if not isinstance(view, TimeSeriesView):
        raise TypeError("HistoricalMean requires a TimeSeriesView (asset-returns block)")
    return view


class _HistoricalMeanModel:
    """Fitted historical-mean model: forecasts the per-asset sample mean over the fit window."""

    def __init__(self, window: int | None) -> None:
        self._window = window

    def capabilities(self) -> set[str]:
        return {capabilities.TO_FORECAST}

    def forecast(self, view: DataView) -> pd.Series:
        rets = _as_tsv(view).returns_frame()
        if self._window is not None:
            rets = rets.tail(self._window)
        return rets.mean()  # per-asset prevailing mean; index = view.assets


[docs] class HistoricalMean: """The prevailing-historical-mean forecaster (Goyal-Welch benchmark). Parameters ---------- window: Trailing window (in calendar steps) for the mean; ``None`` (default) is the expanding prevailing mean — the same quantity the walk-forward engine uses as its OOS R^2 benchmark. """
[docs] def __init__(self, *, window: int | None = None) -> None: if window is not None and window < 1: raise ValueError("window must be >= 1") self.window = window
def fit(self, view: DataView) -> _HistoricalMeanModel: _as_tsv(view) return _HistoricalMeanModel(self.window)