quantlab.portfolio.config.MeanVarianceConfig#
- class quantlab.portfolio.config.MeanVarianceConfig(*, expected_return_label, covariance, risk_aversion, expected_return, turnover_penalty=0.0, min_trade=0.0, weight_cap=1.0, direction='long_only', candidate_top_k=None, max_names=None, mip_gap=None, volatility_label=None, exposure_factors=(), exposure_bounds=<factory>, solver='CLARABEL')[source]#
Bases:
FrozenConfigConfig of
MeanVarianceOptimizer: Markowitz weights with a turnover penalty.The optimiser maximises
w @ mu - risk_aversion / 2 * w @ Sigma @ w - turnover_penalty * |w - w_current|_1, withmuandSigmaon the span ofexpected_return_label.Examples
>>> cfg = MeanVarianceConfig( ... expected_return_label="ret_5", ... covariance=LedoitWolfEstimator(LedoitWolfEstimatorConfig(lookback_bars=252)), ... expected_return=GrinoldExpectedReturn(GrinoldExpectedReturnConfig(ic=0.05)), ... risk_aversion=10.0, weight_cap=0.05, ... ) >>> cfg.direction, cfg.turnover_penalty, cfg.candidate_top_k ('long_only', 0.0, None) >>> cfg.volatility_label is None True
- candidate_top_k = None#
Optimise only over the
candidate_top_ksymbols with the largest expected return (largest absolute one for"long_short") plus every symbol currently held (|w| > HELD_WEIGHT_TOLERANCE); the rest get 0.0.Noneoptimises over every tradable symbol.
- covariance#
The covariance estimator estimating the covariance of one-bar returns.
- direction = 'long_only'#
"long_only": fully invested, non-negative weights (sum(w) = 1)."long_short": dollar-neutral (sum(w) = 0) with gross exposure|w|_1 <= 1, a ceiling, not an equality: the optimiser may leave part of the book uninvested.
- expected_return#
How
expected_return_label’s prediction becomes the expected returnmu(anExpectedReturnModel, ADR 0046):GrinoldExpectedReturn(ic * sigma * z),ZScoreExpectedReturn(ic * median(sigma) * z),RawExpectedReturn(the prediction, in return units) orPredictionExpectedReturn(the prediction as it is).
- expected_return_label#
The label whose prediction gives the expected return; its span sets the horizon of the expected return and the covariance.
- exposure_bounds#
Output name of an
exposure_factorsfactor, or of an exposure of the factor risk model the covariance estimator declares (exposure_names, such asstyle_beta, read fromcontext.risk_exposures), but not of both, to(lower, upper): the book’s exposuresum_i w_i * x_i, the locked positions’ included, is held between the two, to the solver’s tolerance (about 1e-9 with CLARABEL, 1e-5 with OSQP). A candidate without an exposure gets no weight. Empty: no exposure is bounded.
- exposure_factors = ()#
Factors whose outputs
exposure_boundsbounds, such as aBenchmarkBeta; the rule declares them indeclared_inputs(), so the backtest hands their values at each bar incontext.factors.
- max_names = None#
The largest name count of the book decided at each bar: the symbols with a non-zero weight, locked positions included. Each bar is then solved exactly as a mixed-integer program (one boolean per candidate), so
solvermust solve mixed-integer conic programs ("MOSEK", which needs a licence); names the solve leaves out get exactly 0, a held one closed in full whatevermin_trade. Long-only,weight_cap * max_namesmust be at least 1 (near 1 the weights are all near the cap); long-short, long and short names count together and any cap fits, the gross being a ceiling. Withcandidate_top_kthe program is solved on the pool, faster.None: no cap, a convex program.
- min_trade = 0.0#
Smallest trade: a candidate whose solved weight differs from its current one by less than this keeps its current weight, so no order is placed for a solver residue or a tiny adjustment, and a traded target below it (a residue left on a position being sold) is 0. Skipped sells that leave the book above its budget are absorbed by the candidates that do trade, scaled down; when they cannot, those sells are made. A skipped buy leaves its budget in cash, and the bounded exposures may move by about the skipped trades’ total.
"long_only"only; 0 trades every change.
- mip_gap = None#
The relative optimality gap the mixed-integer solve with
max_namesstops at, in(0, 1): the book’s objective is within this fraction of the best book’s. Passed to MOSEK (MSK_DPAR_MIO_TOL_REL_GAP) or GUROBI (MIPGap).Nonekeeps the solver’s default (1e-4 for MOSEK). No time limit is set.
- risk_aversion#
Risk aversion
lambdaof the variance penalty.
- solver = 'CLARABEL'#
The cvxpy solver each bar’s problem is solved with, by its cvxpy name (
cvxpy.installed_solvers()).Noneis refused, since cvxpy’s own choice depends on the installed solvers and the cvxpy version. Our choice:"CLARABEL"(interior point), whose constraints hold to about 1e-9, against about 1e-5 for"OSQP"(first order, the one cvxpy 1.9 picks); it leaves residues of 1e-10 to 1e-6 on the symbols it does not hold, whichmin_tradekeeps from being traded.
- turnover_penalty = 0.0#
Penalty
kappaper unit of one-way turnover against the current weights; 0 trades freely.
- volatility_label = None#
The label whose prediction gives each symbol’s volatility over the span, such as
Volatility; it must have the expected-return label’s span and a"raw"scale. The covariance is then these volatilities around the covariance estimator’s correlations, and thesigmaanexpected_returnmodel reads is the prediction.Nonekeeps the covariance estimator’s own (historical) volatilities.
- weight_cap = 1.0#
Largest absolute weight of one symbol.