quantlab.portfolio.predefined.approximate.mean_variance.decide#

quantlab.portfolio.predefined.approximate.mean_variance.decide(rule, panel, prices=None, stopping=Stopping(tolerance=1e-05, max_iterations=20000), *, posterior=None)[source]#

Return every bar’s mean-variance weights of a decision panel: in time order when they read the holdings, at once otherwise.

mu and the covariance are the per-bar rule’s (see the module docstring).

The candidates are each bar’s predicted symbols, with candidate_top_k only the top candidate_top_k by mu (|mu| long-short), and with a turnover penalty or candidate_top_k also the selectable symbols held. A bar with no candidate, or long-only too few to hold the book under weight_cap, holds (an all-NaN row), as a backtest run holds it. Each bar maximises the per-bar rule’s objective on its candidates in compiled code (_support.mean_variance_kernels), only on the columns some bar of the panel can select: accelerated proximal gradient whose step backtracks below the curvature on the budget’s tangent space, until the residual meets stopping. Long-only on the capped simplex, long-short on sum(w) = 0, |w|_1 <= 1, |w| <= weight_cap.

With max_names every bar’s book holds at most that many names, long and short counted together: each bar is first solved without the cap, and the capped descent starts from that book (and from the holdings, the better book kept); each projection keeps the names whose zeroing costs it most and projects again on them, the steps have no momentum, and a swap search then moves one held name’s weight to an unheld candidate while that lowers the objective (#363). That is a local optimum of the mixed-integer program a backtest run solves exactly.

With a turnover_penalty or candidate_top_k the bars are solved forward in time, each once: its holdings are its book (the last decided bar’s weights, or prices.previous, at their fill prices) marked at its close over the book’s value, cash included; the objective loses turnover_penalty * |w - holdings|_1, which enters the projection as an L1 pull toward the holdings, and the solve starts from the holdings. A held bar leaves the book as it was. Otherwise the bars are independent and solved at once, in parallel except on macOS; a held symbol without a prediction is then not a candidate (the per-bar rule keeps it at mu = 0).

Parameters:
ruleMeanVarianceOptimizer

The rule, bound to the predicted labels (its span read).

panelDecisionPanel

The bars’ decision inputs.

pricesPanelPrices, optional

The book before the first bar and the prices it drifts with; None for no book.

stoppingStopping

The residual at which a bar has converged, and the iterations after which it stops unconverged.

posteriorcallable, optional

The per-rule hook ahead of the kernels, the panel counterpart of MeanVarianceOptimizer.posterior: posterior(rule, panel, problem) returns the PanelProblem to solve, its mu, its covariance (of any rank) and its candidates (predicted) replaced as the rule’s per-bar posterior replaces them; candidate_top_k ranks the returned mu. Unlike the per-bar hook’s NaN, the problem’s expected_return is 0.0 off the candidates, so a hook tells a symbol without a prediction apart by predicted. None (the default) solves the problem as it is. An approximate implementation of a rule built on the optimiser passes its own.

Returns:
PanelSolution

[bars, symbols] weights, each bar’s iterations and whether it converged.

Raises:
RuntimeError

If the rule has not been bound to label specs.

Examples

With panel the two-bar decision panel of the quantlab.portfolio.approximate.estimate_all example (CCC not covered):

>>> from quantlab.portfolio.config import (
...     GrinoldExpectedReturnConfig, LedoitWolfEstimatorConfig, MeanVarianceConfig,
... )
>>> from quantlab.portfolio.predefined.expected_return import GrinoldExpectedReturn
>>> from quantlab.portfolio.predefined.ledoit_wolf import LedoitWolfEstimator
>>> from quantlab.portfolio.predefined.mean_variance import MeanVarianceOptimizer
>>> from quantlab.runs.prediction_panel import LabelSpec
>>> optimizer = MeanVarianceOptimizer(MeanVarianceConfig(
...     expected_return_label="ret_5", risk_aversion=10.0, weight_cap=0.8,
...     expected_return=GrinoldExpectedReturn(GrinoldExpectedReturnConfig(ic=0.05)),
...     covariance=LedoitWolfEstimator(LedoitWolfEstimatorConfig(lookback_bars=20)),
... ))
>>> optimizer.bind([LabelSpec(name="ret_5", scale="raw", delay=1, span=5)])
>>> decide(optimizer, panel).weights.round(3)
array([[0.578, 0.422, 0.   ],
       [0.577, 0.423, 0.   ]])

With a turnover penalty, a book of 0.2 AAA and 0.8 BBB held before the first bar, prices unchanged, pulls both bars toward it:

>>> import dataclasses
>>> penalised = MeanVarianceOptimizer(dataclasses.replace(optimizer.config, turnover_penalty=0.002))
>>> penalised.bind([LabelSpec(name="ret_5", scale="raw", delay=1, span=5)])
>>> book = PanelPrices(
...     previous=np.array([0.2, 0.8, 0.0]), previous_entry=np.ones(3),
...     close=np.ones((2, 3)), entry=np.ones((2, 3)),
... )
>>> decide(penalised, panel, book).weights.round(3)
array([[0.481, 0.519, 0.   ],
       [0.482, 0.518, 0.   ]])