numeraire 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
numeraire/__init__.py ADDED
@@ -0,0 +1,118 @@
1
+ """numeraire — a research framework for empirical asset pricing.
2
+
3
+ The reference unit against which methods are measured. The spine (``numeraire.core``)
4
+ is method-agnostic and depended upon by everything; methods and adapters depend on it,
5
+ never the reverse (enforced by import-linter, see pyproject ``[tool.importlinter]``).
6
+ """
7
+
8
+ from numeraire.core import capabilities
9
+ from numeraire.core.data import CrossSectionView, TimeSeriesView
10
+ from numeraire.core.engine import (
11
+ ForecastOutput,
12
+ PanelWeightsOutput,
13
+ PricingOutput,
14
+ WeightsOutput,
15
+ config_hash,
16
+ pricing_in_sample,
17
+ walk_forward,
18
+ walk_forward_forecast,
19
+ walk_forward_panel,
20
+ walk_forward_pricing,
21
+ )
22
+ from numeraire.core.evaluators import (
23
+ AlphaEvaluator,
24
+ AverageAbsAlphaEvaluator,
25
+ CEQEvaluator,
26
+ ClarkWestEvaluator,
27
+ CrossSectionalR2Evaluator,
28
+ MeanReturnEvaluator,
29
+ OOSR2Evaluator,
30
+ SharpeEvaluator,
31
+ SquaredErrorDiffEvaluator,
32
+ StrategyReturnEvaluator,
33
+ )
34
+ from numeraire.core.protocols import (
35
+ DataView,
36
+ Estimator,
37
+ Evaluator,
38
+ Model,
39
+ Splitter,
40
+ SupportsForecast,
41
+ SupportsPricing,
42
+ SupportsWeights,
43
+ )
44
+ from numeraire.core.registry import (
45
+ available_evaluators,
46
+ get_evaluator,
47
+ register_evaluator,
48
+ )
49
+ from numeraire.core.schema import RESULT_COLUMNS, validate_result
50
+ from numeraire.core.simulate import RebalanceSchedule, SimulationResult, simulate_weights
51
+ from numeraire.core.sorts import SortResult, make_sorts
52
+ from numeraire.core.splitter import WalkForwardSplitter, validation_split
53
+ from numeraire.core.stats import (
54
+ adjust_tests,
55
+ alpha_regression,
56
+ certainty_equivalent,
57
+ clark_west,
58
+ grs_test,
59
+ newey_west_lrv,
60
+ performance_fee,
61
+ return_loss,
62
+ sharpe_diff_test,
63
+ )
64
+
65
+ __all__ = [
66
+ "RESULT_COLUMNS",
67
+ "AlphaEvaluator",
68
+ "AverageAbsAlphaEvaluator",
69
+ "CEQEvaluator",
70
+ "ClarkWestEvaluator",
71
+ "CrossSectionView",
72
+ "CrossSectionalR2Evaluator",
73
+ "DataView",
74
+ "Estimator",
75
+ "Evaluator",
76
+ "ForecastOutput",
77
+ "MeanReturnEvaluator",
78
+ "Model",
79
+ "OOSR2Evaluator",
80
+ "PanelWeightsOutput",
81
+ "PricingOutput",
82
+ "RebalanceSchedule",
83
+ "SharpeEvaluator",
84
+ "SimulationResult",
85
+ "SortResult",
86
+ "Splitter",
87
+ "SquaredErrorDiffEvaluator",
88
+ "StrategyReturnEvaluator",
89
+ "SupportsForecast",
90
+ "SupportsPricing",
91
+ "SupportsWeights",
92
+ "TimeSeriesView",
93
+ "WalkForwardSplitter",
94
+ "WeightsOutput",
95
+ "adjust_tests",
96
+ "alpha_regression",
97
+ "available_evaluators",
98
+ "capabilities",
99
+ "certainty_equivalent",
100
+ "clark_west",
101
+ "config_hash",
102
+ "get_evaluator",
103
+ "grs_test",
104
+ "make_sorts",
105
+ "newey_west_lrv",
106
+ "performance_fee",
107
+ "pricing_in_sample",
108
+ "register_evaluator",
109
+ "return_loss",
110
+ "sharpe_diff_test",
111
+ "simulate_weights",
112
+ "validate_result",
113
+ "validation_split",
114
+ "walk_forward",
115
+ "walk_forward_forecast",
116
+ "walk_forward_panel",
117
+ "walk_forward_pricing",
118
+ ]
@@ -0,0 +1,6 @@
1
+ """numeraire.adapters — thin wrappers making reference libraries conform to core protocols.
2
+
3
+ Glue, not spine — each adapter imports its reference library only at module top level, so installing
4
+ core alone never requires it. Reference-library methods (e.g. IPCA via ``ipca``) ship in extension
5
+ packages such as ``numeraire-zoo``, which declare those heavy dependencies themselves.
6
+ """
@@ -0,0 +1,105 @@
1
+ """skfolio adapter — wrap a skfolio portfolio optimizer as a numeraire ``to_weights`` estimator.
2
+
3
+ `skfolio <https://skfolio.org>`_ (BSD-3) provides mean-risk / hierarchical-risk-parity /
4
+ risk-budgeting optimizers as scikit-learn estimators. This adapter makes one conform to the
5
+ numeraire ``Estimator`` / ``Model`` protocol so it plugs into the walk-forward engine as a peer of
6
+ any native method — **without** adopting skfolio's own cross-validation or walk-forward machinery
7
+ (numeraire owns the out-of-sample loop).
8
+
9
+ The contract (why this stays leak-free):
10
+
11
+ - ``fit(view)`` fits the skfolio estimator on ``view.returns_frame()`` — the exact training window
12
+ the engine hands it — and stores the fitted ``weights_``.
13
+ - ``to_weights(view)`` **broadcasts** those fitted weights across the view's calendar. It never
14
+ calls ``estimator.predict(X_test)``: skfolio's ``predict`` scores a weight vector *on the returns
15
+ it is given*, so feeding it the test window would pour realized test returns into the position —
16
+ a structural look-ahead. Weights come only from ``.weights_`` (a function of the fit window).
17
+
18
+ Through ``walk_forward`` the estimator is re-fit at each origin on that origin's PIT window and the
19
+ resulting weights are applied to the next period, so the broadcast is per-origin and point-in-time.
20
+ The optional ``window`` caps the lookback to the most recent ``window`` rows of whatever the engine
21
+ hands ``fit`` (e.g. a rolling estimation window under an expanding split).
22
+
23
+ skfolio is an **optional** dependency (the ``[skfolio]`` extra); it is imported lazily inside
24
+ ``fit`` so this module imports with or without it installed.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from typing import Any
30
+
31
+ import numpy as np
32
+ import pandas as pd
33
+
34
+ from numeraire.core import capabilities
35
+ from numeraire.core.data import TimeSeriesView
36
+ from numeraire.core.protocols import DataView
37
+
38
+
39
+ def _as_tsv(view: DataView) -> TimeSeriesView:
40
+ if not isinstance(view, TimeSeriesView):
41
+ raise TypeError("the skfolio adapter runs on a TimeSeriesView (asset returns block)")
42
+ return view
43
+
44
+
45
+ class _SkfolioModel:
46
+ """A fitted skfolio portfolio: a single optimal weight vector, broadcast across a calendar."""
47
+
48
+ def __init__(self, weights: pd.Series, meta: dict[str, Any]) -> None:
49
+ self._weights = weights # index = fitted asset labels
50
+ self.meta = meta
51
+
52
+ def capabilities(self) -> set[str]:
53
+ return {capabilities.TO_WEIGHTS}
54
+
55
+ def to_weights(self, view: DataView) -> pd.DataFrame:
56
+ tsv = _as_tsv(view)
57
+ assets = [str(a) for a in tsv.assets]
58
+ w = self._weights.reindex(assets).to_numpy(dtype=np.float64)
59
+ if np.isnan(w).any():
60
+ missing = [a for a, x in zip(assets, w, strict=True) if np.isnan(x)]
61
+ raise ValueError(f"fitted skfolio weights do not cover view assets {missing}")
62
+ vals = np.repeat(w[None, :], len(tsv.calendar), axis=0)
63
+ return pd.DataFrame(vals, index=tsv.calendar, columns=tsv.assets)
64
+
65
+
66
+ class SkfolioWeights:
67
+ """Adapt a skfolio optimizer to numeraire ``to_weights``.
68
+
69
+ ``estimator`` is a skfolio estimator instance (e.g. ``MeanRisk()``, ``RiskBudgeting()``,
70
+ ``HierarchicalRiskParity()``); it is cloned per fit so each origin gets a fresh optimization.
71
+ When ``None``, a default ``skfolio.optimization.MeanRisk`` is used. ``window`` optionally caps
72
+ the estimation lookback to the most recent rows of the fit view.
73
+ """
74
+
75
+ def __init__(self, estimator: Any | None = None, *, window: int | None = None) -> None:
76
+ self.estimator = estimator
77
+ self.window = window
78
+
79
+ def fit(self, view: DataView) -> _SkfolioModel:
80
+ import skfolio
81
+ from sklearn.base import clone
82
+
83
+ tsv = _as_tsv(view)
84
+ if self.estimator is None:
85
+ from skfolio.optimization import MeanRisk
86
+
87
+ est = MeanRisk()
88
+ else:
89
+ est = clone(self.estimator)
90
+ returns = tsv.returns_frame()
91
+ if self.window is not None:
92
+ returns = returns.tail(self.window)
93
+ est.fit(returns)
94
+ weights = pd.Series(
95
+ np.asarray(est.weights_, dtype=np.float64).ravel(),
96
+ index=[str(a) for a in tsv.assets],
97
+ )
98
+ meta = {
99
+ "adapter": "skfolio",
100
+ "skfolio_version": skfolio.__version__,
101
+ "estimator": type(est).__name__,
102
+ "solver": getattr(est, "solver", None),
103
+ "n_train": len(returns),
104
+ }
105
+ return _SkfolioModel(weights, meta)
@@ -0,0 +1,39 @@
1
+ """Universal benchmarks bundled in ``numeraire`` — the reference rules every method is compared to.
2
+
3
+ Four estimators, all framework citizens (they pass ``numeraire.testing.check_estimator``) and all
4
+ registered via the ``numeraire.methods`` entry-point group (dogfooding the open discovery):
5
+
6
+ - :class:`EqualWeight` — 1/N ``to_weights`` (the naive benchmark).
7
+ - :class:`MinVariance` — global minimum-variance ``to_weights`` (sample covariance + window).
8
+ - :class:`MeanVariance` — plug-in mean-variance ``to_weights`` (``S^-1 mu``, explicit norm).
9
+ - :class:`HistoricalMean` — prevailing-historical-mean ``to_forecast`` (Goyal-Welch benchmark).
10
+
11
+ The pure weight functions (:func:`equal_weights`, :func:`minimum_variance_weights`,
12
+ :func:`mean_variance_weights`) are the single source of truth for these formulae — method packages
13
+ (e.g. the naive-diversification reproduction) build on them rather than re-deriving the algebra.
14
+
15
+ Serious constrained optimizers are **not** re-implemented here; they arrive through the optional
16
+ skfolio adapter (``numeraire.adapters``). These baselines are the always-available floor.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from numeraire.baselines.forecast import HistoricalMean
22
+ from numeraire.baselines.weights import (
23
+ EqualWeight,
24
+ MeanVariance,
25
+ MinVariance,
26
+ equal_weights,
27
+ mean_variance_weights,
28
+ minimum_variance_weights,
29
+ )
30
+
31
+ __all__ = [
32
+ "EqualWeight",
33
+ "HistoricalMean",
34
+ "MeanVariance",
35
+ "MinVariance",
36
+ "equal_weights",
37
+ "mean_variance_weights",
38
+ "minimum_variance_weights",
39
+ ]
@@ -0,0 +1,64 @@
1
+ """The historical-mean forecast baseline — the Goyal-Welch OOS reference.
2
+
3
+ The prevailing (expanding) historical mean of the returns block is the benchmark every predictive
4
+ regression is scored against: Goyal-Welch (2008) show it is a stubbornly hard forecast to beat out
5
+ of sample, and the OOS R^2 in :class:`~numeraire.core.evaluators.OOSR2Evaluator` measures MSE
6
+ improvement *relative to it*. The walk-forward forecast engine already computes exactly this
7
+ benchmark column for free at each origin (``train.returns_frame().mean(axis=0)``); this estimator
8
+ exposes the very same quantity as a first-class ``to_forecast`` citizen, so it can be compared,
9
+ registered and run through the engine like any other method (its OOS R^2 against the engine
10
+ benchmark is ~0 by construction — it *is* the benchmark).
11
+
12
+ ``window`` gives the rolling variant (last ``k`` observations); ``None`` (default) is the expanding
13
+ prevailing mean, matching the engine's benchmark convention.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import pandas as pd
19
+
20
+ from numeraire.core import capabilities
21
+ from numeraire.core.data import TimeSeriesView
22
+ from numeraire.core.protocols import DataView
23
+
24
+
25
+ def _as_tsv(view: DataView) -> TimeSeriesView:
26
+ if not isinstance(view, TimeSeriesView):
27
+ raise TypeError("HistoricalMean requires a TimeSeriesView (asset-returns block)")
28
+ return view
29
+
30
+
31
+ class _HistoricalMeanModel:
32
+ """Fitted historical-mean model: forecasts the per-asset sample mean over the fit window."""
33
+
34
+ def __init__(self, window: int | None) -> None:
35
+ self._window = window
36
+
37
+ def capabilities(self) -> set[str]:
38
+ return {capabilities.TO_FORECAST}
39
+
40
+ def forecast(self, view: DataView) -> pd.Series:
41
+ rets = _as_tsv(view).returns_frame()
42
+ if self._window is not None:
43
+ rets = rets.tail(self._window)
44
+ return rets.mean() # per-asset prevailing mean; index = view.assets
45
+
46
+
47
+ class HistoricalMean:
48
+ """The prevailing-historical-mean forecaster (Goyal-Welch benchmark).
49
+
50
+ Parameters
51
+ ----------
52
+ window:
53
+ Trailing window (in calendar steps) for the mean; ``None`` (default) is the expanding
54
+ prevailing mean — the same quantity the walk-forward engine uses as its OOS R^2 benchmark.
55
+ """
56
+
57
+ def __init__(self, *, window: int | None = None) -> None:
58
+ if window is not None and window < 1:
59
+ raise ValueError("window must be >= 1")
60
+ self.window = window
61
+
62
+ def fit(self, view: DataView) -> _HistoricalMeanModel:
63
+ _as_tsv(view)
64
+ return _HistoricalMeanModel(self.window)
@@ -0,0 +1,218 @@
1
+ """Universal weight baselines: the 1/N, minimum-variance and mean-variance rules.
2
+
3
+ These three are the reference points every ``to_weights`` method is measured against — the naive
4
+ benchmark (1/N) plus the two textbook sample-plug-in optimizers. They are bundled in ``numeraire``
5
+ itself (not a method package) because they are method-agnostic and every comparison needs them.
6
+
7
+ Weight functions (the single source of truth; extensions build on these rather than re-deriving):
8
+
9
+ - :func:`equal_weights` — ``w = 1/N`` (needs no estimation; the naive benchmark).
10
+ - :func:`minimum_variance_weights` — global minimum-variance ``w = S^-1 1 / (1' S^-1 1)`` (the
11
+ first-order condition of ``min w'Sw`` s.t. ``1'w = 1``; always well defined for invertible ``S``).
12
+ - :func:`mean_variance_weights` — the plug-in tangency direction ``w ∝ S^-1 mu``, with the
13
+ normalization made **explicit** (``"budget"`` divides by ``1' S^-1 mu`` so weights sum to one;
14
+ ``"none"`` leaves the raw proportional direction). The ``"budget"`` divisor passes through zero
15
+ when the tangency portfolio is nearly cash-neutral — the origin of sample mean-variance's famous
16
+ weight/turnover explosion, which is a property of the rule, not a bug here.
17
+
18
+ The estimators (:class:`EqualWeight`, :class:`MinVariance`, :class:`MeanVariance`) are point-in-time
19
+ citizens: each ``to_weights(view)`` rebalances at every date on ``view.calendar``, estimating the
20
+ sample moments from that date's own trailing window (``window`` caps it to a rolling estimate; the
21
+ default expands from the start). They window internally, so look-ahead is structurally impossible.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from collections.abc import Callable
27
+ from typing import Literal
28
+
29
+ import numpy as np
30
+ import pandas as pd
31
+ from numpy.typing import NDArray
32
+
33
+ from numeraire.core import capabilities
34
+ from numeraire.core.data import TimeSeriesView
35
+ from numeraire.core.protocols import DataView
36
+
37
+ Float = NDArray[np.float64]
38
+ Normalization = Literal["budget", "none"]
39
+
40
+
41
+ # --------------------------------------------------------------------------- weight functions
42
+
43
+
44
+ def equal_weights(n: int) -> Float:
45
+ """The naive 1/N portfolio: ``w_i = 1/n`` (needs no moment estimation)."""
46
+ if n < 1:
47
+ raise ValueError(f"need at least one asset; got n={n}")
48
+ return np.ones(n, dtype=np.float64) / n
49
+
50
+
51
+ def minimum_variance_weights(cov: Float) -> Float:
52
+ """Global minimum-variance weights ``S^-1 1 / (1' S^-1 1)`` — the FOC of ``min w'Sw, 1'w=1``."""
53
+ n = len(cov)
54
+ x = np.linalg.solve(cov, np.ones(n, dtype=np.float64))
55
+ return x / x.sum()
56
+
57
+
58
+ def mean_variance_weights(
59
+ mu: Float, cov: Float, *, normalization: Normalization = "budget"
60
+ ) -> Float:
61
+ """Plug-in mean-variance (tangency) weights ``∝ S^-1 mu``, normalization made explicit.
62
+
63
+ ``normalization``:
64
+
65
+ - ``"budget"`` (default): divide by ``1' S^-1 mu`` so the weights sum to one — the
66
+ DeMiguel-Garlappi-Uppal convention. The divisor passes through zero for a near-cash-neutral
67
+ tangency portfolio, which is why sample mean-variance weights and turnover explode.
68
+ - ``"none"``: the raw proportional direction ``S^-1 mu`` (no budget rescaling).
69
+ """
70
+ x = np.linalg.solve(cov, mu)
71
+ if normalization == "budget":
72
+ return x / x.sum()
73
+ if normalization == "none":
74
+ return x
75
+ raise ValueError(f"normalization must be 'budget' or 'none'; got {normalization!r}")
76
+
77
+
78
+ # --------------------------------------------------------------------------- engine citizens
79
+
80
+
81
+ def _as_tsv(view: DataView) -> TimeSeriesView:
82
+ if not isinstance(view, TimeSeriesView):
83
+ raise TypeError("baseline weight rules run on a TimeSeriesView (asset-returns block)")
84
+ return view
85
+
86
+
87
+ def _rolling_weights(
88
+ view: TimeSeriesView, weight_fn: Callable[[Float], Float], *, window: int | None, min_obs: int
89
+ ) -> pd.DataFrame:
90
+ """Rebalance at every calendar date from that date's trailing window (PIT: windows internally).
91
+
92
+ A date is skipped (warm-up) until at least ``min_obs`` past observations are available, and
93
+ ``weight_fn`` gets the last ``window`` rows (all history if ``window`` is ``None``); it maps a
94
+ ``(T x N)`` block of past returns to an ``N``-vector of weights.
95
+ """
96
+ assets = view.assets
97
+ rows: list[Float] = []
98
+ idx: list[pd.Timestamp] = []
99
+ for t in view.calendar:
100
+ hist = view.window(t).returns_frame().to_numpy(dtype=np.float64)
101
+ if len(hist) < min_obs:
102
+ continue # warm-up
103
+ block = hist if window is None else hist[-window:]
104
+ if len(block) < min_obs:
105
+ continue
106
+ rows.append(weight_fn(block))
107
+ idx.append(t)
108
+ if not rows:
109
+ return pd.DataFrame(columns=assets)
110
+ return pd.DataFrame(np.vstack(rows), index=pd.DatetimeIndex(idx), columns=assets)
111
+
112
+
113
+ class _WeightModel:
114
+ """Fitted (parameter-free, closed-form) baseline weight model over any view's calendar."""
115
+
116
+ def __init__(
117
+ self, weight_fn: Callable[[Float], Float], *, window: int | None, min_obs: int
118
+ ) -> None:
119
+ self._weight_fn = weight_fn
120
+ self._window = window
121
+ self._min_obs = min_obs
122
+
123
+ def capabilities(self) -> set[str]:
124
+ return {capabilities.TO_WEIGHTS}
125
+
126
+ def to_weights(self, view: DataView) -> pd.DataFrame:
127
+ return _rolling_weights(
128
+ _as_tsv(view), self._weight_fn, window=self._window, min_obs=self._min_obs
129
+ )
130
+
131
+
132
+ class EqualWeight:
133
+ """The 1/N benchmark estimator: rebalances to equal weights over ``view.assets`` each period."""
134
+
135
+ def fit(self, view: DataView) -> _WeightModel:
136
+ _as_tsv(view)
137
+ return _WeightModel(lambda b: equal_weights(b.shape[1]), window=None, min_obs=1)
138
+
139
+
140
+ def _resolve_min_obs(explicit: int | None, n_assets: int) -> int:
141
+ """Warm-up length: an explicit floor, else one more row than assets (invertible covariance)."""
142
+ return explicit if explicit is not None else n_assets + 1
143
+
144
+
145
+ class MinVariance:
146
+ """Global minimum-variance estimator: sample covariance from the (optionally windowed) view.
147
+
148
+ Parameters
149
+ ----------
150
+ window:
151
+ Trailing window (in calendar steps) for the sample covariance; ``None`` (default) expands
152
+ from the start. A rolling cap mirrors the skfolio adapter's estimation window.
153
+ min_obs:
154
+ Minimum observations before the first rebalance; ``None`` (default) requires strictly more
155
+ rows than assets, so the sample covariance is non-singular.
156
+ """
157
+
158
+ def __init__(self, *, window: int | None = None, min_obs: int | None = None) -> None:
159
+ if window is not None and window < 2:
160
+ raise ValueError("window must be >= 2 (a covariance needs at least two rows)")
161
+ if min_obs is not None and min_obs < 2:
162
+ raise ValueError("min_obs must be >= 2")
163
+ self.window = window
164
+ self.min_obs = min_obs
165
+
166
+ def fit(self, view: DataView) -> _WeightModel:
167
+ tsv = _as_tsv(view)
168
+
169
+ def _fn(block: Float) -> Float:
170
+ return minimum_variance_weights(np.cov(block, rowvar=False))
171
+
172
+ return _WeightModel(
173
+ _fn, window=self.window, min_obs=_resolve_min_obs(self.min_obs, len(tsv.assets))
174
+ )
175
+
176
+
177
+ class MeanVariance:
178
+ """Plug-in mean-variance estimator: sample ``mu``/``S`` → ``S^-1 mu``, normalization explicit.
179
+
180
+ Parameters
181
+ ----------
182
+ normalization:
183
+ ``"budget"`` (default) divides by ``1' S^-1 mu`` (weights sum to one; DGU convention);
184
+ ``"none"`` returns the raw proportional direction ``S^-1 mu``.
185
+ window, min_obs:
186
+ As for :class:`MinVariance` (rolling estimation window / warm-up; default warm-up is one
187
+ row more than the asset count so the sample covariance is invertible).
188
+ """
189
+
190
+ def __init__(
191
+ self,
192
+ *,
193
+ normalization: Normalization = "budget",
194
+ window: int | None = None,
195
+ min_obs: int | None = None,
196
+ ) -> None:
197
+ if normalization not in ("budget", "none"):
198
+ raise ValueError(f"normalization must be 'budget' or 'none'; got {normalization!r}")
199
+ if window is not None and window < 2:
200
+ raise ValueError("window must be >= 2 (moments need at least two rows)")
201
+ if min_obs is not None and min_obs < 2:
202
+ raise ValueError("min_obs must be >= 2")
203
+ self.normalization: Normalization = normalization
204
+ self.window = window
205
+ self.min_obs = min_obs
206
+
207
+ def fit(self, view: DataView) -> _WeightModel:
208
+ tsv = _as_tsv(view)
209
+ norm = self.normalization
210
+
211
+ def _fn(block: Float) -> Float:
212
+ mu = block.mean(axis=0)
213
+ cov = np.cov(block, rowvar=False)
214
+ return mean_variance_weights(mu, cov, normalization=norm)
215
+
216
+ return _WeightModel(
217
+ _fn, window=self.window, min_obs=_resolve_min_obs(self.min_obs, len(tsv.assets))
218
+ )