Optimization¶
Comprehensive guide to the portfolio optimization module. This module provides 10+ optimizer models spanning convex programming, hierarchical clustering, ensemble methods, robust formulations, and naive baselines. Every model follows the same pattern: frozen @dataclass config + factory function + str, Enum types.
Architecture¶
All optimizers produce sklearn-compatible estimators that expose fit(X) / predict(X) and compose into sklearn.pipeline.Pipeline. The config/factory split enforces a strict boundary:
- Config (
@dataclass(frozen=True)) -- holds only primitives, enums, and nested frozen dataclasses (serializable, hashable). - Factory function -- accepts the config plus any non-serializable objects (prior estimators, numpy arrays, constraint matrices) as keyword arguments.
from optimizer.optimization import MeanRiskConfig, build_mean_risk
# Config: serializable, hashable, suitable for storage/logging
config = MeanRiskConfig.for_max_sharpe()
# Factory: builds the skfolio estimator, accepts non-serializable kwargs
model = build_mean_risk(config, prior_estimator=my_prior)
model.fit(X)
portfolio = model.predict(X)
Model Overview¶
| Model | Config | Factory | Category |
|---|---|---|---|
| Mean-Risk | MeanRiskConfig |
build_mean_risk() |
Convex |
| Risk Budgeting | RiskBudgetingConfig |
build_risk_budgeting() |
Convex |
| Max Diversification | MaxDiversificationConfig |
build_max_diversification() |
Convex |
| HRP | HRPConfig |
build_hrp() |
Hierarchical |
| HERC | HERCConfig |
build_herc() |
Hierarchical |
| NCO | NCOConfig |
build_nco() |
Hierarchical |
| Benchmark Tracker | BenchmarkTrackerConfig |
build_benchmark_tracker() |
Convex |
| Equal Weighted | EqualWeightedConfig |
build_equal_weighted() |
Naive |
| Inverse Volatility | InverseVolatilityConfig |
build_inverse_volatility() |
Naive |
| Stacking | StackingConfig |
build_stacking() |
Ensemble |
| Robust Mean-Risk | RobustConfig |
build_robust_mean_risk() |
Robust |
| DR-CVaR | DRCVaRConfig |
build_dr_cvar() |
Robust |
| Regime-Blended | RegimeRiskConfig |
build_regime_blended_optimizer() |
Regime |
Enums¶
ObjectiveFunctionType¶
Controls what the convex optimizer maximizes or minimizes.
| Value | Description |
|---|---|
MINIMIZE_RISK |
Minimize the chosen risk measure subject to constraints |
MAXIMIZE_RETURN |
Maximize expected return subject to a risk budget |
MAXIMIZE_UTILITY |
Maximize \( \mu^\top w - \frac{\lambda}{2} \rho(w) \) where \( \lambda \) is risk_aversion |
MAXIMIZE_RATIO |
Maximize the return/risk ratio (e.g. Sharpe ratio) |
RiskMeasureType¶
Fifteen convex risk measures available for MeanRisk, RiskBudgeting, BenchmarkTracker, and other convex optimizers.
| Value | Mathematical Definition |
|---|---|
VARIANCE |
\( \sigma^2 = w^\top \Sigma w \) |
SEMI_VARIANCE |
Variance computed only on below-mean returns |
STANDARD_DEVIATION |
\( \sigma = \sqrt{w^\top \Sigma w} \) |
SEMI_DEVIATION |
Standard deviation of below-mean returns |
MEAN_ABSOLUTE_DEVIATION |
\( \text{MAD} = \mathbb{E}[\lvert r_p - \mu_p \rvert] \) |
FIRST_LOWER_PARTIAL_MOMENT |
\( \text{FLPM} = \mathbb{E}[\max(0, \tau - r_p)] \) |
CVAR |
\( \text{CVaR}_\alpha = -\frac{1}{1-\alpha}\int_0^{1-\alpha} F^{-1}(u)\,du \) |
EVAR |
Entropic Value at Risk (tightest upper bound on CVaR from Chernoff inequality) |
WORST_REALIZATION |
\( \max_t(-r_{p,t}) \) -- the worst single-period loss |
CDAR |
Conditional Drawdown at Risk (CVaR applied to the drawdown distribution) |
MAX_DRAWDOWN |
Maximum peak-to-trough decline |
AVERAGE_DRAWDOWN |
Mean of the drawdown series |
EDAR |
Entropic Drawdown at Risk |
ULCER_INDEX |
\( \sqrt{\frac{1}{T}\sum_t d_t^2} \) where \( d_t \) is the drawdown at time \( t \) |
GINI_MEAN_DIFFERENCE |
\( \text{GMD} = \frac{1}{T^2}\sum_{i \neq j}\lvert r_i - r_j \rvert \) |
ExtraRiskMeasureType¶
Seven non-convex risk measures available exclusively for hierarchical methods (HRP, HERC) which do not require convexity.
| Value | Description |
|---|---|
VALUE_AT_RISK |
\( \text{VaR}_\alpha = -F^{-1}(1-\alpha) \) |
DRAWDOWN_AT_RISK |
VaR applied to the drawdown distribution |
ENTROPIC_RISK_MEASURE |
Entropic risk measure |
FOURTH_CENTRAL_MOMENT |
\( \mathbb{E}[(r - \mu)^4] \) -- captures kurtosis |
FOURTH_LOWER_PARTIAL_MOMENT |
Fourth moment of below-mean returns |
SKEW |
Third standardized central moment |
KURTOSIS |
Fourth standardized central moment |
DistanceType¶
Distance metrics for hierarchical clustering in HRP, HERC, and NCO.
| Value | Description |
|---|---|
PEARSON |
\( d_{ij} = \sqrt{\frac{1}{2}(1 - \rho_{ij})} \) (default) |
KENDALL |
Kendall rank correlation distance |
SPEARMAN |
Spearman rank correlation distance |
COVARIANCE |
Covariance-based distance (requires a covariance estimator) |
DISTANCE_CORRELATION |
Non-linear distance correlation (captures non-linear dependencies) |
MUTUAL_INFORMATION |
Information-theoretic distance |
LinkageMethodType¶
Linkage methods for agglomerative hierarchical clustering.
| Value | Description |
|---|---|
WARD |
Minimize within-cluster variance (default, requires Euclidean distance) |
SINGLE |
Nearest-neighbor linkage |
COMPLETE |
Farthest-neighbor linkage |
AVERAGE |
Average linkage (UPGMA) |
WEIGHTED |
Weighted average linkage (WPGMA) |
CENTROID |
Centroid linkage |
MEDIAN |
Median linkage |
RatioMeasureType¶
Ratio measures for scoring and ensemble quantile selection. Includes 18 standard skfolio ratio measures plus a custom INFORMATION_RATIO (active return / tracking error).
Sub-Configs¶
DistanceConfig¶
Configures the distance estimator used by hierarchical methods.
| Field | Type | Default | Description |
|---|---|---|---|
distance_type |
DistanceType |
PEARSON |
Distance metric |
absolute |
bool |
False |
Apply absolute transformation to correlation matrix |
power |
float |
1.0 |
Power transformation exponent |
threshold |
float |
0.5 |
Distance correlation threshold (only for DISTANCE_CORRELATION) |
from optimizer.optimization import DistanceConfig, DistanceType
# Spearman distance (robust to outliers)
dist_cfg = DistanceConfig(
distance_type=DistanceType.SPEARMAN,
absolute=True,
)
ClusteringConfig¶
Configures hierarchical clustering used by HRP, HERC, and NCO.
| Field | Type | Default | Description |
|---|---|---|---|
max_clusters |
int or None |
None |
Maximum number of flat clusters. None uses the Two-Order Difference Gap Statistic heuristic |
linkage_method |
LinkageMethodType |
WARD |
Linkage method for the dendrogram |
from optimizer.optimization import ClusteringConfig, LinkageMethodType
cluster_cfg = ClusteringConfig(
max_clusters=5,
linkage_method=LinkageMethodType.COMPLETE,
)
1. Mean-Risk Optimization (MeanRiskConfig)¶
The workhorse of the module. Solves the general convex mean-risk program:
where \( \rho \) is any convex risk measure from RiskMeasureType.
Configuration Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
objective |
ObjectiveFunctionType |
MINIMIZE_RISK |
Objective function |
risk_measure |
RiskMeasureType |
VARIANCE |
Risk measure |
risk_aversion |
float |
1.0 |
Risk-aversion coefficient for MAXIMIZE_UTILITY |
efficient_frontier_size |
int or None |
None |
Number of points on the efficient frontier (None = single portfolio) |
min_weights |
float or None |
0.0 |
Lower bound on asset weights |
max_weights |
float or None |
1.0 |
Upper bound on asset weights |
budget |
float or None |
1.0 |
Portfolio budget (sum of weights) |
max_short |
float or None |
None |
Maximum short position |
max_long |
float or None |
None |
Maximum long position |
cardinality |
int or None |
None |
Maximum number of assets |
transaction_costs |
float |
0.0 |
Linear transaction costs |
management_fees |
float |
0.0 |
Linear management fees |
max_tracking_error |
float or None |
None |
Maximum tracking error vs benchmark |
l1_coef |
float |
0.0 |
L1 regularization coefficient (promotes sparsity) |
l2_coef |
float |
0.0 |
L2 regularization coefficient (shrinks weights toward zero) |
risk_free_rate |
float |
0.0 |
Risk-free rate for ratio objectives |
cvar_beta |
float |
0.95 |
CVaR confidence level |
evar_beta |
float |
0.95 |
EVaR confidence level |
cdar_beta |
float |
0.95 |
CDaR confidence level |
edar_beta |
float |
0.95 |
EDaR confidence level |
solver |
str |
"CLARABEL" |
CVXPY solver name |
solver_params |
dict or None |
None |
Additional solver parameters |
prior_config |
MomentEstimationConfig or None |
None |
Inner prior configuration |
Presets¶
from optimizer.optimization import MeanRiskConfig
# Minimum-variance portfolio
config = MeanRiskConfig.for_min_variance()
# Maximum Sharpe ratio
config = MeanRiskConfig.for_max_sharpe()
# Maximum utility with custom risk aversion
config = MeanRiskConfig.for_max_utility(risk_aversion=2.0)
# Minimum CVaR at 99% confidence
config = MeanRiskConfig.for_min_cvar(beta=0.99)
# Efficient frontier with 30 points
config = MeanRiskConfig.for_efficient_frontier(size=30)
Factory: build_mean_risk()¶
from optimizer.optimization import MeanRiskConfig, build_mean_risk
# Basic usage
model = build_mean_risk(MeanRiskConfig.for_max_sharpe())
model.fit(X)
portfolio = model.predict(X)
# With prior estimator and factor constraints
from optimizer.moments import build_prior, MomentEstimationConfig
from optimizer.factors import build_factor_exposure_constraints
prior = build_prior(MomentEstimationConfig.for_shrunk_denoised())
constraints = build_factor_exposure_constraints(...)
model = build_mean_risk(
config,
prior_estimator=prior,
factor_exposure_constraints=constraints,
previous_weights=old_weights, # for transaction cost optimization
)
Factory kwargs (non-serializable, not stored in config):
- prior_estimator -- skfolio BasePrior instance
- factor_exposure_constraints -- FactorExposureConstraints (injects left_inequality / right_inequality)
- previous_weights -- numpy array for turnover-aware optimization
- groups -- asset group labels
- linear_constraints -- additional linear constraints
Short-Selling Example¶
config = MeanRiskConfig(
objective=ObjectiveFunctionType.MAXIMIZE_RATIO,
risk_measure=RiskMeasureType.VARIANCE,
min_weights=-0.3, # allow up to 30% short per asset
max_weights=0.5, # max 50% long per asset
max_short=0.5, # total short exposure <= 50%
max_long=1.5, # total long exposure <= 150%
budget=1.0, # net exposure = 100%
)
Cardinality-Constrained Example¶
config = MeanRiskConfig(
objective=ObjectiveFunctionType.MINIMIZE_RISK,
risk_measure=RiskMeasureType.VARIANCE,
cardinality=15, # at most 15 assets
l1_coef=0.001, # L1 regularization for sparsity
)
2. Risk Budgeting (RiskBudgetingConfig)¶
Risk parity and generalized risk budgeting. Each asset contributes a pre-specified share of total portfolio risk:
where \( b_i \) is the risk budget for asset \( i \) (summing to 1). When \( b_i = 1/n \) for all \( i \), this is risk parity.
Configuration Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
risk_measure |
RiskMeasureType |
VARIANCE |
Risk measure |
min_weights |
float or None |
0.0 |
Lower bound on asset weights |
max_weights |
float or None |
1.0 |
Upper bound on asset weights |
risk_free_rate |
float |
0.0 |
Risk-free rate |
cvar_beta |
float |
0.95 |
CVaR confidence level |
evar_beta |
float |
0.95 |
EVaR confidence level |
cdar_beta |
float |
0.95 |
CDaR confidence level |
edar_beta |
float |
0.95 |
EDaR confidence level |
solver |
str |
"CLARABEL" |
CVXPY solver name |
solver_params |
dict or None |
None |
Additional solver parameters |
prior_config |
MomentEstimationConfig or None |
None |
Inner prior configuration |
Presets¶
from optimizer.optimization import RiskBudgetingConfig
# Equal risk contribution (risk parity) with variance
config = RiskBudgetingConfig.for_risk_parity()
# Risk parity with CVaR
config = RiskBudgetingConfig.for_cvar_parity(beta=0.95)
# Risk parity with CDaR
config = RiskBudgetingConfig.for_cdar_parity(beta=0.95)
Factory: build_risk_budgeting()¶
The risk_budget array is passed as a factory kwarg because numpy arrays are not hashable in frozen dataclasses.
import numpy as np
from optimizer.optimization import RiskBudgetingConfig, build_risk_budgeting
# Equal risk parity (default when risk_budget=None)
model = build_risk_budgeting(RiskBudgetingConfig.for_risk_parity())
model.fit(X)
# Custom risk budgets: 60% risk to equities, 40% to bonds
budgets = np.array([0.15, 0.15, 0.15, 0.15, 0.20, 0.20])
model = build_risk_budgeting(
RiskBudgetingConfig.for_cvar_parity(),
risk_budget=budgets,
)
Gotcha: When risk_budget=None, skfolio assigns equal budgets (1/n per asset). You do not need to manually construct the equal-weight array.
3. Maximum Diversification (MaxDiversificationConfig)¶
Maximizes the diversification ratio:
where \( \sigma \) is the vector of individual asset volatilities and \( \Sigma \) is the covariance matrix.
Configuration Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
min_weights |
float or None |
0.0 |
Lower bound on asset weights |
max_weights |
float or None |
1.0 |
Upper bound on asset weights |
budget |
float or None |
1.0 |
Portfolio budget |
max_short |
float or None |
None |
Maximum short position |
max_long |
float or None |
None |
Maximum long position |
cardinality |
int or None |
None |
Maximum number of assets |
l1_coef |
float |
0.0 |
L1 regularization |
l2_coef |
float |
0.0 |
L2 regularization |
risk_free_rate |
float |
0.0 |
Risk-free rate |
solver |
str |
"CLARABEL" |
CVXPY solver |
solver_params |
dict or None |
None |
Solver parameters |
prior_config |
MomentEstimationConfig or None |
None |
Prior configuration |
Usage¶
from optimizer.optimization import MaxDiversificationConfig, build_max_diversification
config = MaxDiversificationConfig(l2_coef=0.01)
model = build_max_diversification(config)
model.fit(X)
4. Hierarchical Risk Parity -- HRP (HRPConfig)¶
Hierarchical Risk Parity (Lopez de Prado, 2016) avoids matrix inversion entirely. It builds a hierarchical clustering dendrogram from asset distances, then allocates risk by recursively bisecting the dendrogram and inverse-variance weighting each split.
Algorithm Steps¶
- Compute a distance matrix from asset returns (e.g. Pearson correlation distance)
- Build a hierarchical clustering dendrogram using a linkage method
- Quasi-diagonalize the covariance matrix according to the dendrogram ordering
- Recursively bisect the dendrogram, allocating weights inversely proportional to cluster risk at each split
Configuration Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
risk_measure |
RiskMeasureType |
VARIANCE |
Convex risk measure |
extra_risk_measure |
ExtraRiskMeasureType or None |
None |
Non-convex risk measure (overrides risk_measure when set) |
min_weights |
float or None |
0.0 |
Lower bound on asset weights |
max_weights |
float or None |
1.0 |
Upper bound on asset weights |
distance_config |
DistanceConfig or None |
None |
Distance estimator configuration |
clustering_config |
ClusteringConfig or None |
None |
Clustering configuration |
prior_config |
MomentEstimationConfig or None |
None |
Prior configuration |
Presets¶
from optimizer.optimization import HRPConfig
config = HRPConfig.for_variance()
config = HRPConfig.for_cvar()
Usage with Custom Distance and Clustering¶
from optimizer.optimization import (
HRPConfig,
build_hrp,
DistanceConfig,
DistanceType,
ClusteringConfig,
LinkageMethodType,
ExtraRiskMeasureType,
)
config = HRPConfig(
risk_measure=RiskMeasureType.CVAR,
distance_config=DistanceConfig(
distance_type=DistanceType.SPEARMAN,
absolute=True,
),
clustering_config=ClusteringConfig(
max_clusters=5,
linkage_method=LinkageMethodType.COMPLETE,
),
)
model = build_hrp(config)
model.fit(X)
Using Non-Convex Risk Measures¶
HRP and HERC support non-convex risk measures via extra_risk_measure. When set, it overrides risk_measure:
5. Hierarchical Equal Risk Contribution -- HERC (HERCConfig)¶
HERC (Thomas et al., 2018) extends HRP by equalizing risk contributions within each cluster, similar to risk budgeting but applied to the hierarchical tree structure. Unlike HRP, HERC can use a solver for the intra-cluster allocation step.
Configuration Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
risk_measure |
RiskMeasureType |
VARIANCE |
Convex risk measure |
extra_risk_measure |
ExtraRiskMeasureType or None |
None |
Non-convex risk measure (overrides risk_measure) |
min_weights |
float or None |
0.0 |
Lower bound |
max_weights |
float or None |
1.0 |
Upper bound |
solver |
str |
"CLARABEL" |
CVXPY solver |
solver_params |
dict or None |
None |
Solver parameters |
distance_config |
DistanceConfig or None |
None |
Distance configuration |
clustering_config |
ClusteringConfig or None |
None |
Clustering configuration |
prior_config |
MomentEstimationConfig or None |
None |
Prior configuration |
Presets¶
from optimizer.optimization import HERCConfig
config = HERCConfig.for_variance()
config = HERCConfig.for_cvar()
Usage¶
from optimizer.optimization import HERCConfig, build_herc
config = HERCConfig(
risk_measure=RiskMeasureType.CVAR,
clustering_config=ClusteringConfig(max_clusters=4),
)
model = build_herc(config)
model.fit(X)
6. Nested Clusters Optimization -- NCO (NCOConfig)¶
NCO (Lopez de Prado, 2019) addresses the instability of mean-variance by decomposing the optimization into intra-cluster and inter-cluster stages:
- Cluster assets using hierarchical clustering
- Run an inner optimizer within each cluster
- Run an outer optimizer across the cluster-level portfolios
Configuration Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
quantile |
float |
0.5 |
Quantile for portfolio selection across CV folds |
n_jobs |
int or None |
None |
Number of parallel jobs |
distance_config |
DistanceConfig or None |
None |
Distance configuration |
clustering_config |
ClusteringConfig or None |
None |
Clustering configuration |
Usage¶
The inner and outer estimators are passed as factory kwargs because they are not serializable:
from optimizer.optimization import (
NCOConfig,
build_nco,
build_mean_risk,
MeanRiskConfig,
)
inner = build_mean_risk(MeanRiskConfig.for_min_variance())
outer = build_mean_risk(MeanRiskConfig.for_max_sharpe())
config = NCOConfig(quantile=0.5)
model = build_nco(
config,
inner_estimator=inner,
outer_estimator=outer,
)
model.fit(X)
7. Benchmark Tracker (BenchmarkTrackerConfig)¶
Minimizes tracking error against a benchmark index. The benchmark returns are passed as y in fit(X, y).
Configuration Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
risk_measure |
RiskMeasureType |
STANDARD_DEVIATION |
Risk measure for tracking error |
min_weights |
float or None |
0.0 |
Lower bound |
max_weights |
float or None |
1.0 |
Upper bound |
max_short |
float or None |
None |
Maximum short |
max_long |
float or None |
None |
Maximum long |
cardinality |
int or None |
None |
Maximum assets |
transaction_costs |
float |
0.0 |
Transaction costs |
management_fees |
float |
0.0 |
Management fees |
l1_coef |
float |
0.0 |
L1 regularization |
l2_coef |
float |
0.0 |
L2 regularization |
risk_free_rate |
float |
0.0 |
Risk-free rate |
solver |
str |
"CLARABEL" |
CVXPY solver |
solver_params |
dict or None |
None |
Solver parameters |
prior_config |
MomentEstimationConfig or None |
None |
Prior configuration |
Usage¶
from optimizer.optimization import BenchmarkTrackerConfig, build_benchmark_tracker
config = BenchmarkTrackerConfig(
cardinality=50, # replicate benchmark with at most 50 stocks
l1_coef=0.001, # sparse tracking
)
model = build_benchmark_tracker(config)
# benchmark_returns is a 1-D array/Series aligned with X
model.fit(X, y=benchmark_returns)
portfolio = model.predict(X)
Gotcha: Benchmark returns must be passed as y in fit(X, y), not as part of the config or factory kwargs.
8. Equal Weighted (EqualWeightedConfig)¶
The naive 1/N allocation. Assigns identical weight \( w_i = 1/N \) to each asset. No estimation is required, making it immune to estimation error. Serves as a strong baseline that is surprisingly hard to beat out of sample (DeMiguel et al., 2009).
Usage¶
from optimizer.optimization import EqualWeightedConfig, build_equal_weighted
model = build_equal_weighted(EqualWeightedConfig())
model.fit(X)
EqualWeightedConfig has no parameters.
9. Inverse Volatility (InverseVolatilityConfig)¶
Weights each asset inversely proportional to its estimated volatility:
The volatility estimates come from the diagonal of the covariance matrix provided by the prior estimator.
Configuration Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
prior_config |
MomentEstimationConfig or None |
None |
Prior configuration (covariance estimator determines volatility) |
Usage¶
from optimizer.optimization import InverseVolatilityConfig, build_inverse_volatility
from optimizer.moments import MomentEstimationConfig
config = InverseVolatilityConfig(
prior_config=MomentEstimationConfig.for_shrunk_denoised(),
)
model = build_inverse_volatility(config)
model.fit(X)
10. Stacking Optimization (StackingConfig)¶
Ensemble method that combines multiple sub-optimizers via a meta-optimizer. Each sub-optimizer produces a portfolio, and the meta-optimizer allocates across those portfolios.
Configuration Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
quantile |
float |
0.5 |
Quantile for portfolio selection across CV folds |
quantile_measure |
RatioMeasureType |
SHARPE_RATIO |
Ratio measure for quantile selection |
n_jobs |
int or None |
None |
Number of parallel jobs |
cv |
int or None |
None |
Cross-validation folds (None = no CV) |
Usage¶
The estimators list and final_estimator are passed as factory kwargs:
from optimizer.optimization import (
StackingConfig,
build_stacking,
build_mean_risk,
build_hrp,
MeanRiskConfig,
HRPConfig,
RatioMeasureType,
)
sub_optimizers = [
("min_var", build_mean_risk(MeanRiskConfig.for_min_variance())),
("max_sharpe", build_mean_risk(MeanRiskConfig.for_max_sharpe())),
("hrp", build_hrp(HRPConfig.for_variance())),
]
meta = build_mean_risk(MeanRiskConfig.for_min_variance())
config = StackingConfig(
quantile=0.5,
quantile_measure=RatioMeasureType.SHARPE_RATIO,
cv=5,
)
model = build_stacking(
config,
estimators=sub_optimizers,
final_estimator=meta,
)
model.fit(X)
Default estimators: When estimators=None, the factory defaults to [("mean_risk", MeanRisk()), ("hrp", HierarchicalRiskParity())].
Robust Variants¶
11. Robust Mean-Risk (RobustConfig)¶
Hedges against estimation error in the expected return vector by constructing an ellipsoidal uncertainty set around the sample mean and optimizing for the worst-case expected return within that set.
Uncertainty Set for Expected Returns¶
The ellipsoidal uncertainty set is:
where: - \( \hat{\mu} \) is the estimated mean vector (sample or shrinkage) - \( S_\mu = \hat{\Sigma} / T \) is the estimation error covariance of the sample mean - \( \kappa \) is the robustness parameter (larger values produce more conservative, diversified portfolios)
The worst-case expected return within \( U_\mu \) is:
The penalty term \( \kappa \cdot \| S_\mu^{1/2} w \|_2 \) grows with the portfolio's exposure to estimation uncertainty, naturally encouraging diversification.
Kappa-Confidence Level Mapping¶
The parameter \( \kappa \) relates to the chi-squared confidence level via:
where \( n \) is the number of assets. Since \( n \) is only known at fit time, the conversion from \( \kappa \) to confidence_level is deferred to the fit() call.
Configuration Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
kappa |
float |
1.0 |
Ellipsoidal uncertainty radius for \( \mu \). kappa=0 recovers standard MeanRisk exactly |
cov_uncertainty |
bool |
False |
Also apply covariance uncertainty set |
cov_uncertainty_method |
str |
"bootstrap" |
"bootstrap" (stationary block bootstrap) or "empirical" (formula-based) |
B |
int |
500 |
Number of bootstrap resamples (only for "bootstrap" method) |
block_size |
int |
21 |
Expected block length for stationary bootstrap (~1 trading month) |
bootstrap_alpha |
float |
0.05 |
Significance level for covariance uncertainty ellipsoid |
mean_risk_config |
MeanRiskConfig or None |
None |
Embedded mean-risk configuration |
Presets¶
| Preset | kappa | cov_uncertainty | Use Case |
|---|---|---|---|
for_conservative() |
2.0 | False |
High estimation uncertainty (short history, non-stationary) |
for_moderate() |
1.0 | False |
Balanced trade-off |
for_aggressive() |
0.5 | False |
Closer to standard MeanRisk |
for_bootstrap_covariance() |
1.0 | True |
Hedges against both mean and covariance estimation error |
Usage¶
from optimizer.optimization import RobustConfig, build_robust_mean_risk, MeanRiskConfig
# Conservative: strong robustness
model = build_robust_mean_risk(RobustConfig.for_conservative())
model.fit(X)
# kappa=0: identical to standard MeanRisk (no penalty)
baseline = build_robust_mean_risk(RobustConfig(kappa=0.0))
# Robust max-Sharpe with bootstrap covariance uncertainty
config = RobustConfig(
kappa=1.5,
cov_uncertainty=True,
cov_uncertainty_method="bootstrap",
B=1000,
block_size=21,
mean_risk_config=MeanRiskConfig.for_max_sharpe(),
)
model = build_robust_mean_risk(config)
model.fit(X)
Standalone Bootstrap Covariance Utility¶
The module also exposes bootstrap_covariance_uncertainty() for standalone analysis of covariance estimation uncertainty:
from optimizer.optimization import bootstrap_covariance_uncertainty
result = bootstrap_covariance_uncertainty(
returns,
B=500,
block_size=21,
alpha=0.05,
seed=42,
)
print(f"Frobenius-norm confidence radius: {result.delta:.4f}")
print(f"Sample covariance shape: {result.cov_hat.shape}")
print(f"Bootstrap samples shape: {result.cov_samples.shape}") # (500, n, n)
The Frobenius-norm confidence set is \( \{ \Sigma : \| \Sigma - \hat{\Sigma} \|_F \leq \delta \} \) where \( \delta \) is the \( (1-\alpha) \) quantile of bootstrap Frobenius distances.
12. Distributionally Robust CVaR (DRCVaRConfig)¶
Minimizes the worst-case CVaR over all probability distributions within a Wasserstein ball of radius \( \varepsilon \) centered at the empirical distribution:
The tractable SOCP reformulation (Esfahani and Kuhn, 2018) is solved via skfolio's DistributionallyRobustCVaR, which exposes this as a risk-aversion utility:
Configuration Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
epsilon |
float |
0.001 |
Wasserstein ball radius. Larger values = more conservative. epsilon=0 = standard CVaR |
alpha |
float |
0.95 |
CVaR confidence level |
risk_aversion |
float |
1.0 |
Risk-aversion coefficient \( \lambda \) (ignored when epsilon=0) |
norm |
int |
2 |
Wasserstein norm order. Only L2 is supported |
min_weights |
float or None |
0.0 |
Lower bound |
max_weights |
float or None |
1.0 |
Upper bound |
budget |
float or None |
1.0 |
Portfolio budget |
max_short |
float or None |
None |
Maximum short |
max_long |
float or None |
None |
Maximum long |
risk_free_rate |
float |
0.0 |
Risk-free rate |
solver |
str |
"CLARABEL" |
CVXPY solver. MOSEK preferred for large instances |
solver_params |
dict or None |
None |
Solver parameters |
prior_config |
MomentEstimationConfig or None |
None |
Prior configuration |
Presets¶
| Preset | epsilon | Description |
|---|---|---|
for_conservative() |
0.01 |
Wider ball, more robust against tail risk misspecification |
for_standard() |
0.001 |
Moderate hedge against distribution misspecification |
Dispatch Behavior¶
The factory build_dr_cvar() dispatches to different skfolio classes based on epsilon:
- epsilon = 0: Returns
MeanRisk(MINIMIZE_RISK, CVAR)-- identical to standard empirical CVaR minimization. - epsilon > 0: Returns
DistributionallyRobustCVaR-- solves the Wasserstein DRO reformulation.
Usage¶
from optimizer.optimization import DRCVaRConfig, build_dr_cvar
# Conservative DRO-CVaR
model = build_dr_cvar(DRCVaRConfig.for_conservative())
model.fit(X)
# epsilon=0 -> standard CVaR (exact equivalence)
baseline = build_dr_cvar(DRCVaRConfig(epsilon=0.0))
# Custom: 99% CVaR, wider Wasserstein ball
config = DRCVaRConfig(
epsilon=0.05,
alpha=0.99,
risk_aversion=2.0,
)
model = build_dr_cvar(config)
model.fit(X)
Gotcha: Only norm=2 (L2 Wasserstein) is supported. Setting any other value raises ValueError at config construction via __post_init__ validation.
13. Regime-Blended Optimization (RegimeRiskConfig)¶
HMM-driven regime-conditional risk measure selection and risk budgeting. Uses a fitted Hidden Markov Model to select the risk measure based on the current market regime.
Blended Risk Measure¶
The probability-weighted blended risk is:
where \( \gamma_T(s) = P(z_T = s \mid r_{1:T}) \) is the filtered state probability from the HMM, and \( \rho_s \) is the regime-specific risk measure for state \( s \).
Configuration Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
regime_measures |
tuple[RiskMeasureType, ...] |
(required) | One risk measure per HMM state. Must match HMMResult.n_states |
hmm_config |
HMMConfig |
HMMConfig() |
HMM hyper-parameters |
cvar_beta |
float |
0.95 |
CVaR confidence level |
Presets¶
| Preset | States | Risk Measures | Description |
|---|---|---|---|
for_calm_stress() |
2 | Variance, CVaR | Low-vol regime uses variance; stress regime uses CVaR |
for_calm_stress_drawdown() |
2 | Variance, CDaR | Low-vol regime uses variance; stress regime uses CDaR |
for_three_regimes() |
3 | Variance, MAD, CVaR | Calm/normal/stress with increasing tail sensitivity |
Blended Optimizer¶
Because skfolio's MeanRisk requires a single convex risk measure, build_regime_blended_optimizer() selects the risk measure of the dominant regime (the state with the highest current probability):
from optimizer.moments import HMMConfig, fit_hmm
from optimizer.optimization import RegimeRiskConfig, build_regime_blended_optimizer
# Fit HMM
hmm_result = fit_hmm(returns, HMMConfig(n_states=2))
# Build optimizer using dominant regime's risk measure
config = RegimeRiskConfig.for_calm_stress()
model = build_regime_blended_optimizer(config, hmm_result)
model.fit(X)
Blended Risk Computation¶
For analytics and monitoring, compute_blended_risk_measure() computes the full probability-weighted blended risk:
from optimizer.optimization import compute_blended_risk_measure
risk = compute_blended_risk_measure(
returns,
weights,
hmm_result,
regime_measures=(RiskMeasureType.VARIANCE, RiskMeasureType.CVAR),
cvar_beta=0.95,
)
Regimes with fewer than 5 observations fall back to full-sample risk computation.
Regime-Conditional Risk Budgets¶
build_regime_risk_budgeting() computes a probability-weighted blended budget vector and passes it to build_risk_budgeting():
import numpy as np
from optimizer.optimization import (
RiskBudgetingConfig,
build_regime_risk_budgeting,
)
# Per-regime budget vectors
calm_budget = np.array([0.25, 0.25, 0.25, 0.25]) # equal in calm
stress_budget = np.array([0.10, 0.10, 0.40, 0.40]) # tilt to safe assets in stress
model = build_regime_risk_budgeting(
RiskBudgetingConfig.for_risk_parity(),
hmm_result,
regime_budgets=[calm_budget, stress_budget],
)
model.fit(X)
Common Patterns¶
Passing Prior Estimators¶
All convex optimizers accept a prior_estimator factory kwarg. When None, the factory checks config.prior_config and builds a prior from it. If both are None, skfolio's default empirical prior is used.
from optimizer.moments import MomentEstimationConfig, build_prior
from optimizer.optimization import MeanRiskConfig, build_mean_risk
# Option 1: via config (serializable)
config = MeanRiskConfig(
prior_config=MomentEstimationConfig.for_shrunk_denoised(),
)
model = build_mean_risk(config)
# Option 2: via factory kwarg (non-serializable, takes precedence)
prior = build_prior(MomentEstimationConfig.for_adaptive())
model = build_mean_risk(config, prior_estimator=prior)
Factor Exposure Constraints¶
build_mean_risk() accepts a factor_exposure_constraints kwarg that injects left_inequality and right_inequality matrices:
from optimizer.factors import build_factor_exposure_constraints
from optimizer.optimization import MeanRiskConfig, build_mean_risk
constraints = build_factor_exposure_constraints(
factor_scores=scores,
target_exposures=targets,
tolerance=0.1,
)
model = build_mean_risk(
MeanRiskConfig.for_min_variance(),
factor_exposure_constraints=constraints,
)
Explicit left_inequality / right_inequality entries in kwargs take precedence over the constraints object.
Pipeline Integration¶
All optimizers are sklearn-compatible and compose into pipelines:
from sklearn.pipeline import Pipeline
from optimizer.optimization import build_mean_risk, MeanRiskConfig
pipe = Pipeline([
("optimizer", build_mean_risk(MeanRiskConfig.for_max_sharpe())),
])
pipe.fit(X)
Nested parameter access uses sklearn's __ notation:
# Access nested parameters for tuning
pipe.get_params()["optimizer__l2_coef"]
# Set parameters for grid search
param_grid = {
"optimizer__l2_coef": [0.0, 0.001, 0.01],
"optimizer__risk_aversion": [0.5, 1.0, 2.0],
}
Solver Notes¶
All convex optimizers default to solver="CLARABEL", an open-source interior-point solver. For large instances or specific problem structures:
| Solver | License | Best For |
|---|---|---|
CLARABEL |
Open source (Apache 2.0) | General-purpose default |
MOSEK |
Commercial | Large-scale SOCP/SDP, DR-CVaR |
SCS |
Open source | Large sparse problems |
ECOS |
Open source | Small to medium conic programs |
Pass solver parameters via solver_params:
Gotchas and Tips¶
-
Non-serializable objects are factory kwargs, not config fields. Prior estimators, risk budget arrays, inner/outer estimators (NCO), and estimator lists (Stacking) must be passed to the factory function, not stored in the config.
-
kappa=0andepsilon=0recover standard models exactly.RobustConfig(kappa=0.0)produces the same result asbuild_mean_risk().DRCVaRConfig(epsilon=0.0)produces the same result asMeanRisk(MINIMIZE_RISK, CVAR). -
HRP/HERC support non-convex risk measures; convex optimizers do not. Use
ExtraRiskMeasureTypeonly withHRPConfigandHERCConfig. Whenextra_risk_measureis set, it overridesrisk_measure. -
Benchmark returns are
y, not part of the config. ForBenchmarkTracker, always callmodel.fit(X, y=benchmark_returns). -
Regime measures must match HMM states.
len(config.regime_measures)must equalhmm_result.n_states, or aConfigurationErroris raised. -
DR-CVaR only supports L2 norm. Setting
normto anything other than2raisesValueErrorat construction time. -
Stacking defaults. When
estimators=None, the factory defaults to[("mean_risk", MeanRisk()), ("hrp", HierarchicalRiskParity())]with skfolio defaults. -
Cardinality constraints make the problem mixed-integer. Using
cardinalitymay significantly increase solve time. Consider L1 regularization (l1_coef) as a convex relaxation alternative. -
Transaction costs require
previous_weights. Thetransaction_costsfield inMeanRiskConfigpenalizes turnover relative toprevious_weights, which must be passed as a factory kwarg. -
ClusteringConfig
max_clusters=Noneuses automatic selection. The Two-Order Difference Gap Statistic heuristic determines the optimal number of clusters automatically.