Source code for numeraire.adapters.skfolio_adapter
"""skfolio adapter — wrap a skfolio portfolio optimizer as a numeraire ``to_weights`` estimator.
`skfolio <https://skfolio.org>`_ (BSD-3) provides mean-risk / hierarchical-risk-parity /
risk-budgeting optimizers as scikit-learn estimators. This adapter makes one conform to the
numeraire ``Estimator`` / ``Model`` protocol so it plugs into the walk-forward engine as a peer of
any native method — **without** adopting skfolio's own cross-validation or walk-forward machinery
(numeraire owns the out-of-sample loop).
The contract (why this stays leak-free):
- ``fit(view)`` fits the skfolio estimator on ``view.returns_frame()`` — the exact training window
the engine hands it — and stores the fitted ``weights_``.
- ``to_weights(view)`` **broadcasts** those fitted weights across the view's calendar. It never
calls ``estimator.predict(X_test)``: skfolio's ``predict`` scores a weight vector *on the returns
it is given*, so feeding it the test window would pour realized test returns into the position —
a structural look-ahead. Weights come only from ``.weights_`` (a function of the fit window).
Through ``backtest_weights`` the estimator is re-fit at each origin on that origin's PIT window and
the resulting weights are applied to the next period, so the broadcast is per-origin and
point-in-time.
The optional ``window`` caps the lookback to the most recent ``window`` rows of whatever the engine
hands ``fit`` (e.g. a rolling estimation window under an expanding split).
skfolio is an **optional** dependency (the ``[skfolio]`` extra); it is imported lazily inside
``fit`` so this module imports with or without it installed.
"""
from __future__ import annotations
from typing import Any
import numpy as np
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("the skfolio adapter runs on a TimeSeriesView (asset returns block)")
return view
class _SkfolioModel:
"""A fitted skfolio portfolio: a single optimal weight vector, broadcast across a calendar."""
def __init__(self, weights: pd.Series, meta: dict[str, Any]) -> None:
self._weights = weights # index = fitted asset labels
self.meta = meta
def capabilities(self) -> set[str]:
return {capabilities.TO_WEIGHTS}
def to_weights(self, view: DataView) -> pd.DataFrame:
tsv = _as_tsv(view)
assets = [str(a) for a in tsv.assets]
w = self._weights.reindex(assets).to_numpy(dtype=np.float64)
if np.isnan(w).any():
missing = [a for a, x in zip(assets, w, strict=True) if np.isnan(x)]
raise ValueError(f"fitted skfolio weights do not cover view assets {missing}")
vals = np.repeat(w[None, :], len(tsv.calendar), axis=0)
return pd.DataFrame(vals, index=tsv.calendar, columns=tsv.assets)
[docs]
class SkfolioWeights:
"""Adapt a skfolio optimizer to numeraire ``to_weights``.
``estimator`` is a skfolio estimator instance (e.g. ``MeanRisk()``, ``RiskBudgeting()``,
``HierarchicalRiskParity()``); it is cloned per fit so each origin gets a fresh optimization.
When ``None``, a default ``skfolio.optimization.MeanRisk`` is used. ``window`` optionally caps
the estimation lookback to the most recent rows of the fit view.
"""
[docs]
def __init__(self, estimator: Any | None = None, *, window: int | None = None) -> None:
self.estimator = estimator
self.window = window
def fit(self, view: DataView) -> _SkfolioModel:
import skfolio
from sklearn.base import clone
tsv = _as_tsv(view)
if self.estimator is None:
from skfolio.optimization import MeanRisk
est = MeanRisk()
else:
est = clone(self.estimator)
returns = tsv.returns_frame()
if self.window is not None:
returns = returns.tail(self.window)
est.fit(returns)
weights = pd.Series(
np.asarray(est.weights_, dtype=np.float64).ravel(),
index=[str(a) for a in tsv.assets],
)
meta = {
"adapter": "skfolio",
"skfolio_version": skfolio.__version__,
"estimator": type(est).__name__,
"solver": getattr(est, "solver", None),
"n_train": len(returns),
}
return _SkfolioModel(weights, meta)