"""tidyfinance-backed loaders for standard (non-vintage) sources.
tidyfinance (MIT) is the primary backend: Fama-French, Goyal-Welch, FRED, JKP, q-factors, OSAP.
These loaders return **tidy frames** (the package convention — see :mod:`numeraire_dataset.builders`
for the vintage-aware FRED-MD builder that tidyfinance does not cover) plus a ``data_vintage``
provenance stamp. :func:`to_timeseries_view` optionally turns a frame into a numeraire point-in-time
view; it imports ``numeraire`` lazily, so the frame loaders stay numeraire-independent.
For a *golden reproduction*, pin a fixture instead of a live pull — tidyfinance fetches the current
release, which drifts, and the live predictor sets differ slightly from any one paper's (e.g.
Goyal-Welch has no ``dfr``). Pin the ``tidyfinance`` version so a pull is reproducible.
"""
from __future__ import annotations
from importlib.metadata import version
from typing import TYPE_CHECKING, Any
import pandas as pd
import tidyfinance as tf
if TYPE_CHECKING:
from numeraire.core.data import TimeSeriesView
_TF_VERSION = version("tidyfinance")
[docs]
def data_vintage(dataset: str) -> str:
"""Provenance stamp for a live tidyfinance pull (``tidyfinance:<dataset>@<version>``)."""
return f"tidyfinance:{dataset}@{_TF_VERSION}"
[docs]
def load_ff_factors(
*, freq: str = "monthly", start_date: str = "1926-07-01", end_date: str = "2023-12-31"
) -> pd.DataFrame:
"""Fama-French 3-factor tidy frame (``date, mkt_excess, smb, hml, risk_free``), in decimals.
``mkt_excess`` is the excess market return, ``risk_free`` the 1-month T-bill — the canonical
academic market/risk-free pair.
"""
dataset = f"factors_ff_3_{freq}"
return tf.download_data( # pyright: ignore[reportAttributeAccessIssue] # no stubs
domain="factors_ff", dataset=dataset, start_date=start_date, end_date=end_date
)
[docs]
def load_goyal_welch(
*, freq: str = "monthly", start_date: str = "1926-07-01", end_date: str = "2023-12-31"
) -> pd.DataFrame:
"""Goyal-Welch equity-premium frame: ``date, rp_div`` (excess market) + macro predictors."""
dataset = f"macro_predictors_{freq}"
return tf.download_data( # pyright: ignore[reportAttributeAccessIssue] # no stubs
domain="macro_predictors", dataset=dataset, start_date=start_date, end_date=end_date
)
[docs]
def to_timeseries_view(
df: pd.DataFrame,
*,
ret_col: str,
feature_cols: list[str] | None = None,
date_col: str = "date",
asset: str = "mkt",
horizon: int = 1,
) -> TimeSeriesView:
"""A tidy ``(date, ret, features…)`` frame → single-asset numeraire ``TimeSeriesView``.
``feature_cols=None`` uses every column except ``date_col`` and ``ret_col``. Rows with any NaN
in the selected columns are dropped and the index is sorted. Requires the ``numeraire`` extra
(imported lazily so the frame loaders above do not depend on numeraire).
"""
from numeraire.core.data import TimeSeriesView # lazy: keep frame loaders numeraire-free
feats = feature_cols
if feats is None:
feats = [c for c in df.columns if c not in (date_col, ret_col)]
clean = df.set_index(date_col)[[ret_col, *feats]].dropna().sort_index()
idx = pd.DatetimeIndex(pd.to_datetime(clean.index), name="date")
returns = pd.DataFrame({asset: clean[ret_col].to_numpy()}, index=idx)
features = pd.DataFrame(clean[feats].to_numpy(), index=idx, columns=feats)
return TimeSeriesView(returns, features, horizon=horizon)
[docs]
def load_gw_view(
*,
freq: str = "monthly",
start_date: str = "1926-07-01",
end_date: str = "2023-12-31",
predictors: list[str] | None = None,
horizon: int = 1,
) -> tuple[Any, str]:
"""Goyal-Welch view (excess ``rp_div`` + predictors) + ``data_vintage``, ready for VoC/1-A.
Convenience over :func:`load_goyal_welch` + :func:`to_timeseries_view`; needs the ``numeraire``
extra. Return type is the numeraire ``TimeSeriesView`` (typed ``Any`` to avoid a hard import).
"""
df = load_goyal_welch(freq=freq, start_date=start_date, end_date=end_date)
view = to_timeseries_view(df, ret_col="rp_div", feature_cols=predictors, horizon=horizon)
return view, data_vintage(f"macro_predictors_{freq}")