distributionally_robust
Distributionally robust optimization (DRO) addresses a fundamental challenge in portfolio management: the true distribution of asset returns is unknown and estimates from historical data are subject to sampling error. Rather than optimizing with respect to a single estimated distribution which can lead to severe out-of-sample underperformance when the estimate is poor, DRO optimizes against the worst-case distribution within an ambiguity set of plausible distributions centered around an empirical or reference distribution.
OPES leverages dual formulations of selected DRO variants, reducing the resulting problems to tractable, and in many cases, convex minimization programs. This includes formulations drawn from both established literature and recent, cutting-edge work, some of which may not yet be fully peer-reviewed. While several dual representations are well-known (e.g. the Kantorovich-Rubinstein duality), others remain comparatively less visible in the existing literature.
KLRobustKelly
class KLRobustKelly(fraction=1.0, radius=0.01)
Kullback-Leibler Ambiguity Kelly Criterion.
Maximizes the expected logarithmic wealth under the worst-case probability distribution within a specified KL-divergence radius. The distributionally robust Kelly criterion addresses estimation error in growth-optimal portfolios by maximizing expected log growth against worst-case distributions within a KL ambiguity set. The KL-robust Kelly criterion produces portfolios that are more diversified than the standard Kelly portfolio, trading off some growth rate for robustness against distributional uncertainty.
Args
radius(float, optional): The size of the uncertainty set (KL-divergence bound). Larger values indicate higher uncertainty. Defaults to0.01.fraction(float, optional): kelly fractional exposure to the market. Must be within (0,1]. Lower values bet less aggressively. Defaults to1.0.
Methods
clean_weights
def clean_weights(threshold=1e-08)
Cleans the portfolio weights by setting very small positions to zero.
Any weight whose absolute value is below the specified threshold is replaced with zero.
This helps remove negligible allocations while keeping the array structure intact. This method
requires portfolio optimization (optimize() method) to take place for self.weights to be
defined other than None.
Warning:
This method modifies the existing portfolio weights in place. After cleaning, re-optimization is required to recover the original weights.
Args
threshold(float, optional): Float specifying the minimum absolute weight to retain. Defaults to1e-8.
Returns:
numpy.ndarray: Cleaned and re-normalized portfolio weight vector.
Raises
PortfolioError: If weights have not been calculated via optimization.
Notes:
- Weights are cleaned using absolute values, making this method compatible with long-short portfolios.
- Re-normalization ensures the portfolio remains properly scaled after cleaning.
- Increasing threshold promotes sparsity but may materially alter the portfolio composition.
optimize
def optimize(data=None, weight_bounds=(0, 1), w=None)
Solves the Hu and Hong KL-Kelly dual objective:
Args
data(pd.DataFrame): Ticker price data in either multi-index or single-index formats. Examples are given below:# Single-Index Example Ticker TSLA NVDA GME PFE AAPL ... Date 2015-01-02 14.620667 0.483011 6.288958 18.688917 24.237551 ... 2015-01-05 14.006000 0.474853 6.460137 18.587513 23.554741 ... 2015-01-06 14.085333 0.460456 6.268492 18.742599 23.556952 ... 2015-01-07 14.063333 0.459257 6.195926 18.999102 23.887287 ... 2015-01-08 14.041333 0.476533 6.268492 19.386841 24.805082 ... ... # Multi-Index Example Structure (OHLCV) Columns: + Ticker (e.g. GME, PFE, AAPL, ...) - Open - High - Low - Close - Volumeweight_bounds(tuple, optional): Boundary constraints for asset weights. Values must be of the format(lesser, greater)with0 <= |lesser|, |greater| <= 1. Defaults to(0,1).w(None or np.ndarray, optional): Initial weight vector for warm starts. Mainly used for backtesting and not recommended for user input. Defaults toNone.
Returns:
np.ndarray: Vector of optimized portfolio weights.
Raises
DataError: For any data mismatch during integrity check.PortfolioError: For any invalid portfolio variable inputs during integrity check.OptimizationError: IfSLSQPsolver fails to solve.
Example:
# Importing the dro Kelly module
from opes.objectives import KLRobustKelly
# Let this be your ticker data
training_data = some_data()
# Initialize with custom fractional exposure and KL divergence radius
kl_kelly = KLRobustKelly(fraction=0.8, radius=0.02)
# Optimize portfolio with custom weight bounds
weights = kl_kelly.optimize(data=training_data, weight_bounds=(0.05, 0.75))
stats
def stats()
Calculates and returns portfolio concentration and diversification statistics.
These statistics help users to inspect portfolio's overall concentration in
allocation. For the method to work, the optimizer must have been initialized, i.e.
the optimize() method should have been called at least once for self.weights
to be defined other than None.
Returns:
- A
dictcontaining the following keys:'tickers'(list): A list of tickers used for optimization.'weights'(np.ndarry): Portfolio weights, output from optimization.'portfolio_entropy'(float): Shannon entropy computed on portfolio weights.'herfindahl_index'(float): Herfindahl Index value, computed on portfolio weights.'gini_coefficient'(float): Gini Coefficient value, computed on portfolio weights.'absolute_max_weight'(float): Absolute maximum allocation for an asset.
Raises
PortfolioError: If weights have not been calculated via optimization.
Notes:
- All statistics are computed on absolute normalized weights (within the simplex), ensuring compatibility with long-short portfolios.
- This method is diagnostic only and does not modify portfolio weights.
- For meaningful interpretation, use these metrics in conjunction with risk and performance measures.
KLRobustMaxMean
class KLRobustMaxMean(radius=0.01)
Kullback-Leibler Ambiguity Maximum Mean optimization.
Optimizes the expected return under the worst-case probability distribution within a KL-divergence uncertainty ball (radius) around the empirical distribution. This problem was analyzed by Hu and Hong in their comprehensive study of KL-constrained distributionally robust optimization, who showed it admits a tractable convex reformulation through Fenchel duality and change-of-measure techniques.
Args
radius(float, optional): The size of the uncertainty set (KL-divergence bound). Larger values indicate higher uncertainty. Defaults to0.01.
Methods
clean_weights
def clean_weights(threshold=1e-08)
Cleans the portfolio weights by setting very small positions to zero.
Any weight whose absolute value is below the specified threshold is replaced with zero.
This helps remove negligible allocations while keeping the array structure intact. This method
requires portfolio optimization (optimize() method) to take place for self.weights to be
defined other than None.
Warning:
This method modifies the existing portfolio weights in place. After cleaning, re-optimization is required to recover the original weights.
Args
threshold(float, optional): Float specifying the minimum absolute weight to retain. Defaults to1e-8.
Returns:
numpy.ndarray: Cleaned and re-normalized portfolio weight vector.
Raises
PortfolioError: If weights have not been calculated via optimization.
Notes:
- Weights are cleaned using absolute values, making this method compatible with long-short portfolios.
- Re-normalization ensures the portfolio remains properly scaled after cleaning.
- Increasing threshold promotes sparsity but may materially alter the portfolio composition.
optimize
def optimize(data=None, weight_bounds=(0, 1), w=None)
Solves the KL-maximum-mean dual objective:
Uses the log-sum-exp technique to solve for numerical stability.
Args
data(pd.DataFrame): Ticker price data in either multi-index or single-index formats. Examples are given below:# Single-Index Example Ticker TSLA NVDA GME PFE AAPL ... Date 2015-01-02 14.620667 0.483011 6.288958 18.688917 24.237551 ... 2015-01-05 14.006000 0.474853 6.460137 18.587513 23.554741 ... 2015-01-06 14.085333 0.460456 6.268492 18.742599 23.556952 ... 2015-01-07 14.063333 0.459257 6.195926 18.999102 23.887287 ... 2015-01-08 14.041333 0.476533 6.268492 19.386841 24.805082 ... ... # Multi-Index Example Structure (OHLCV) Columns: + Ticker (e.g. GME, PFE, AAPL, ...) - Open - High - Low - Close - Volumeweight_bounds(tuple, optional): Boundary constraints for asset weights. Values must be of the format(lesser, greater)with0 <= |lesser|, |greater| <= 1. Defaults to(0,1).w(None or np.ndarray, optional): Initial weight vector for warm starts. Mainly used for backtesting and not recommended for user input. Defaults toNone.
Returns:
np.ndarray: Vector of optimized portfolio weights.
Raises
DataError: For any data mismatch during integrity check.PortfolioError: For any invalid portfolio variable inputs during integrity check.OptimizationError: IfSLSQPsolver fails to solve.
Example:
# Importing the dro maximum mean module
from opes.objectives import KLRobustMaxMean
# Let this be your ticker data
training_data = some_data()
# Initialize with KL divergence radius
kl_maxmean = KLRobustMaxMean(radius=0.02)
# Optimize portfolio with custom weight bounds
weights = kl_maxmean.optimize(data=training_data, weight_bounds=(0.05, 0.75))
stats
def stats()
Calculates and returns portfolio concentration and diversification statistics.
These statistics help users to inspect portfolio's overall concentration in
allocation. For the method to work, the optimizer must have been initialized, i.e.
the optimize() method should have been called at least once for self.weights
to be defined other than None.
Returns:
- A
dictcontaining the following keys:'tickers'(list): A list of tickers used for optimization.'weights'(np.ndarry): Portfolio weights, output from optimization.'portfolio_entropy'(float): Shannon entropy computed on portfolio weights.'herfindahl_index'(float): Herfindahl Index value, computed on portfolio weights.'gini_coefficient'(float): Gini Coefficient value, computed on portfolio weights.'absolute_max_weight'(float): Absolute maximum allocation for an asset.
Raises
PortfolioError: If weights have not been calculated via optimization.
Notes:
- All statistics are computed on absolute normalized weights (within the simplex), ensuring compatibility with long-short portfolios.
- This method is diagnostic only and does not modify portfolio weights.
- For meaningful interpretation, use these metrics in conjunction with risk and performance measures.
WassRobustMaxMean
class WassRobustMaxMean(radius=0.01, ground_norm=2)
Wasserstein Ambiguity Maximum Mean optimization.
Maximum mean return under Wasserstein uncertainty has been studied extensively in the robust optimization literature. The Kantorovich-Rubinstein duality theorem provides an explicit dual reformulation.
Args
radius(float, optional): The size of the uncertainty set (Wasserstein distance bound). Larger values indicate higher uncertainty. Defaults to0.01.ground_norm(int, optional): Wasserstein ground norm. Used to find the dual norm for the dual objective. Must be a positive integer. Defaults to2.
Methods
clean_weights
def clean_weights(threshold=1e-08)
Cleans the portfolio weights by setting very small positions to zero.
Any weight whose absolute value is below the specified threshold is replaced with zero.
This helps remove negligible allocations while keeping the array structure intact. This method
requires portfolio optimization (optimize() method) to take place for self.weights to be
defined other than None.
Warning:
This method modifies the existing portfolio weights in place. After cleaning, re-optimization is required to recover the original weights.
Args
threshold(float, optional): Float specifying the minimum absolute weight to retain. Defaults to1e-8.
Returns:
numpy.ndarray: Cleaned and re-normalized portfolio weight vector.
Raises
PortfolioError: If weights have not been calculated via optimization.
Notes:
- Weights are cleaned using absolute values, making this method compatible with long-short portfolios.
- Re-normalization ensures the portfolio remains properly scaled after cleaning.
- Increasing threshold promotes sparsity but may materially alter the portfolio composition.
optimize
def optimize(
data=None,
weight_bounds=(0, 1),
w=None,
custom_mean=None
)
Solves the Kantorovich-Rubinstein dual objective for type-1 Wasserstein distances:
Args
data(pd.DataFrame): Ticker price data in either multi-index or single-index formats. Examples are given below:# Single-Index Example Ticker TSLA NVDA GME PFE AAPL ... Date 2015-01-02 14.620667 0.483011 6.288958 18.688917 24.237551 ... 2015-01-05 14.006000 0.474853 6.460137 18.587513 23.554741 ... 2015-01-06 14.085333 0.460456 6.268492 18.742599 23.556952 ... 2015-01-07 14.063333 0.459257 6.195926 18.999102 23.887287 ... 2015-01-08 14.041333 0.476533 6.268492 19.386841 24.805082 ... ... # Multi-Index Example Structure (OHLCV) Columns: + Ticker (e.g. GME, PFE, AAPL, ...) - Open - High - Low - Close - Volumeweight_bounds(tuple, optional): Boundary constraints for asset weights. Values must be of the format(lesser, greater)with0 <= |lesser|, |greater| <= 1. Defaults to(0,1).w(None or np.ndarray, optional): Initial weight vector for warm starts. Mainly used for backtesting and not recommended for user input. Defaults toNone.custom_mean(None or np.ndarray, optional): Custom mean vector. Can be used to inject externally generated mean vectors (eg. Black-Litterman). Defaults toNone.
Returns:
np.ndarray: Vector of optimized portfolio weights.
Raises
DataError: For any data mismatch during integrity check.PortfolioError: For any invalid portfolio variable inputs during integrity check.OptimizationError: IfSLSQPsolver fails to solve.
Example:
# Importing the dro maximum mean module
from opes.objectives import WassRobustMaxMean
# Let this be your ticker data
training_data = some_data()
# Let this be your custom mean vector
mean_v = customMean()
# Initialize with ground norm and Wasserstein radius
wass_maxmean = WassRobustMaxMean(radius=0.04, ground_norm=3)
# Optimize portfolio with custom weight bounds and mean vector
weights = wass_maxmean.optimize(data=training_data, weight_bounds=(0.05, 0.75), custom_mean=mean_v)
stats
def stats()
Calculates and returns portfolio concentration and diversification statistics.
These statistics help users to inspect portfolio's overall concentration in
allocation. For the method to work, the optimizer must have been initialized, i.e.
the optimize() method should have been called at least once for self.weights
to be defined other than None.
Returns:
- A
dictcontaining the following keys:'tickers'(list): A list of tickers used for optimization.'weights'(np.ndarry): Portfolio weights, output from optimization.'portfolio_entropy'(float): Shannon entropy computed on portfolio weights.'herfindahl_index'(float): Herfindahl Index value, computed on portfolio weights.'gini_coefficient'(float): Gini Coefficient value, computed on portfolio weights.'absolute_max_weight'(float): Absolute maximum allocation for an asset.
Raises
PortfolioError: If weights have not been calculated via optimization.
Notes:
- All statistics are computed on absolute normalized weights (within the simplex), ensuring compatibility with long-short portfolios.
- This method is diagnostic only and does not modify portfolio weights.
- For meaningful interpretation, use these metrics in conjunction with risk and performance measures.
WassRobustMeanVariance
class WassRobustMeanVariance(risk_aversion=0.3, radius=0.01, ground_norm=2)
Wasserstein Ambiguity Mean-Variance Optimization.
Extends the classical mean-variance framework by incorporating distributional uncertainty using the Wasserstein distance, following the dual reformulation approach of Blanchet et al. Instead of optimizing expected return and variance under a single estimated distribution, the robust formulation considers the worst-case trade-off over a Wasserstein ambiguity set around the empirical data. The resulting dual problem remains tractable and convex, allowing robustness to be introduced without sacrificing computational feasibility. This approach mitigates sensitivity to estimation error in both the mean and covariance and yields portfolios with more reliable out-of-sample behavior under model misspecification.
Args
risk_aversion(float, optional): Risk-aversion coefficient. Higher values emphasize risk minimization, while lower values favor return seeking. Usually greater than0. Defaults to0.3.radius(float, optional): The size of the uncertainty set (Wasserstein distance bound). Larger values indicate higher uncertainty. Defaults to0.01.ground_norm(int, optional): Wasserstein ground norm. Used to find the dual norm for the dual objective. Must be a positive integer. Defaults to2.
Methods
clean_weights
def clean_weights(threshold=1e-08)
Cleans the portfolio weights by setting very small positions to zero.
Any weight whose absolute value is below the specified threshold is replaced with zero.
This helps remove negligible allocations while keeping the array structure intact. This method
requires portfolio optimization (optimize() method) to take place for self.weights to be
defined other than None.
Warning:
This method modifies the existing portfolio weights in place. After cleaning, re-optimization is required to recover the original weights.
Args
threshold(float, optional): Float specifying the minimum absolute weight to retain. Defaults to1e-8.
Returns:
numpy.ndarray: Cleaned and re-normalized portfolio weight vector.
Raises
PortfolioError: If weights have not been calculated via optimization.
Notes:
- Weights are cleaned using absolute values, making this method compatible with long-short portfolios.
- Re-normalization ensures the portfolio remains properly scaled after cleaning.
- Increasing threshold promotes sparsity but may materially alter the portfolio composition.
optimize
def optimize(
data=None,
weight_bounds=(0, 1),
w=None,
custom_mean=None,
custom_cov=None
)
Solves the Wasserstein Ambiguity Mean-Variance dual objective:
Args
data(pd.DataFrame): Ticker price data in either multi-index or single-index formats. Examples are given below:# Single-Index Example Ticker TSLA NVDA GME PFE AAPL ... Date 2015-01-02 14.620667 0.483011 6.288958 18.688917 24.237551 ... 2015-01-05 14.006000 0.474853 6.460137 18.587513 23.554741 ... 2015-01-06 14.085333 0.460456 6.268492 18.742599 23.556952 ... 2015-01-07 14.063333 0.459257 6.195926 18.999102 23.887287 ... 2015-01-08 14.041333 0.476533 6.268492 19.386841 24.805082 ... ... # Multi-Index Example Structure (OHLCV) Columns: + Ticker (e.g. GME, PFE, AAPL, ...) - Open - High - Low - Close - Volumeweight_bounds(tuple, optional): Boundary constraints for asset weights. Values must be of the format(lesser, greater)with0 <= |lesser|, |greater| <= 1. Defaults to(0,1).w(None or np.ndarray, optional): Initial weight vector for warm starts. Mainly used for backtesting and not recommended for user input. Defaults toNone.custom_mean(None or np.ndarray, optional): Custom mean vector. Can be used to inject externally generated mean vectors (eg. Black-Litterman). Defaults toNone.custom_cov(None or array-like of shape (n_assets, n_assets), optional): Custom covariance matrix. Can be used to inject externally generated covariance matrices (eg. Ledoit-Wolf). Defaults toNone.
Returns:
np.ndarray: Vector of optimized portfolio weights.
Raises
DataError: For any data mismatch during integrity check.PortfolioError: For any invalid portfolio variable inputs during integrity check.OptimizationError: IfSLSQPsolver fails to solve.
Example:
# Importing the dro mean-variance module
from opes.objectives import WassRobustMeanVariance
# Let this be your ticker data
training_data = some_data()
# Let these be your custom mean vector and covariance matrix
mean_v = customMean()
cov_m = customCov()
# Initialize with risk aversion, ground norm and Wasserstein radius
wass_mean_variance = WassRobustMeanVariance(risk_aversion=0.5, radius=0.04, ground_norm=3)
# Optimize portfolio with custom weight bounds, mean vector and covariance matrix
weights = wass_mean_variance.optimize(data=training_data, weight_bounds=(0.05, 0.75), custom_mean=mean_v, custom_cov=cov_m)
stats
def stats()
Calculates and returns portfolio concentration and diversification statistics.
These statistics help users to inspect portfolio's overall concentration in
allocation. For the method to work, the optimizer must have been initialized, i.e.
the optimize() method should have been called at least once for self.weights
to be defined other than None.
Returns:
- A
dictcontaining the following keys:'tickers'(list): A list of tickers used for optimization.'weights'(np.ndarry): Portfolio weights, output from optimization.'portfolio_entropy'(float): Shannon entropy computed on portfolio weights.'herfindahl_index'(float): Herfindahl Index value, computed on portfolio weights.'gini_coefficient'(float): Gini Coefficient value, computed on portfolio weights.'absolute_max_weight'(float): Absolute maximum allocation for an asset.
Raises
PortfolioError: If weights have not been calculated via optimization.
Notes:
- All statistics are computed on absolute normalized weights (within the simplex), ensuring compatibility with long-short portfolios.
- This method is diagnostic only and does not modify portfolio weights.
- For meaningful interpretation, use these metrics in conjunction with risk and performance measures.
WassRobustMinVariance
class WassRobustMinVariance(radius=0.01, ground_norm=2)
Wasserstein Ambiguity Minimum Variance optimization.
Builds on the distributionally robust optimization framework developed by Blanchet et al. The method extends the classical GMV portfolio by minimizing the worst-case portfolio variance over a Wasserstein ambiguity set centered at the empirical return distribution. Through duality, this worst-case problem admits a tractable reformulation that preserves convexity while explicitly controlling sensitivity to distributional misspecification. As a result, Wasserstein-robust GMV portfolios exhibit improved stability and out-of-sample performance relative to nominal GMV.
Args
radius(float, optional): The size of the uncertainty set (Wasserstein distance bound). Larger values indicate higher uncertainty. Defaults to0.01.ground_norm(int, optional): Wasserstein ground norm. Used to find the dual norm for the dual objective. Must be a positive integer. Defaults to2.
Methods
clean_weights
def clean_weights(threshold=1e-08)
Cleans the portfolio weights by setting very small positions to zero.
Any weight whose absolute value is below the specified threshold is replaced with zero.
This helps remove negligible allocations while keeping the array structure intact. This method
requires portfolio optimization (optimize() method) to take place for self.weights to be
defined other than None.
Warning:
This method modifies the existing portfolio weights in place. After cleaning, re-optimization is required to recover the original weights.
Args
threshold(float, optional): Float specifying the minimum absolute weight to retain. Defaults to1e-8.
Returns:
numpy.ndarray: Cleaned and re-normalized portfolio weight vector.
Raises
PortfolioError: If weights have not been calculated via optimization.
Notes:
- Weights are cleaned using absolute values, making this method compatible with long-short portfolios.
- Re-normalization ensures the portfolio remains properly scaled after cleaning.
- Increasing threshold promotes sparsity but may materially alter the portfolio composition.
optimize
def optimize(
data=None,
weight_bounds=(0, 1),
w=None,
custom_cov=None
)
Solves the Wasserstein Ambiguity Minimum Variance dual objective:
Args
data(pd.DataFrame): Ticker price data in either multi-index or single-index formats. Examples are given below:# Single-Index Example Ticker TSLA NVDA GME PFE AAPL ... Date 2015-01-02 14.620667 0.483011 6.288958 18.688917 24.237551 ... 2015-01-05 14.006000 0.474853 6.460137 18.587513 23.554741 ... 2015-01-06 14.085333 0.460456 6.268492 18.742599 23.556952 ... 2015-01-07 14.063333 0.459257 6.195926 18.999102 23.887287 ... 2015-01-08 14.041333 0.476533 6.268492 19.386841 24.805082 ... ... # Multi-Index Example Structure (OHLCV) Columns: + Ticker (e.g. GME, PFE, AAPL, ...) - Open - High - Low - Close - Volumeweight_bounds(tuple, optional): Boundary constraints for asset weights. Values must be of the format(lesser, greater)with0 <= |lesser|, |greater| <= 1. Defaults to(0,1).w(None or np.ndarray, optional): Initial weight vector for warm starts. Mainly used for backtesting and not recommended for user input. Defaults toNone.custom_cov(None or array-like of shape (n_assets, n_assets), optional): Custom covariance matrix. Can be used to inject externally generated covariance matrices (eg. Ledoit-Wolf). Defaults toNone.
Returns:
np.ndarray: Vector of optimized portfolio weights.
Raises
DataError: For any data mismatch during integrity check.PortfolioError: For any invalid portfolio variable inputs during integrity check.OptimizationError: IfSLSQPsolver fails to solve.
Example:
# Importing the dro minimum variance module
from opes.objectives import WassRobustMinVariance
# Let this be your ticker data
training_data = some_data()
# Let this be your custom covariance matrix
cov_m = customCov()
# Initialize with ground norm and Wasserstein radius
wass_minvariance = WassRobustMinVariance(radius=0.04, ground_norm=3)
# Optimize portfolio with custom weight bounds and covariance matrix
weights = wass_minvariance.optimize(data=training_data, weight_bounds=(0.05, 0.75), custom_cov=cov_m)
stats
def stats()
Calculates and returns portfolio concentration and diversification statistics.
These statistics help users to inspect portfolio's overall concentration in
allocation. For the method to work, the optimizer must have been initialized, i.e.
the optimize() method should have been called at least once for self.weights
to be defined other than None.
Returns:
- A
dictcontaining the following keys:'tickers'(list): A list of tickers used for optimization.'weights'(np.ndarry): Portfolio weights, output from optimization.'portfolio_entropy'(float): Shannon entropy computed on portfolio weights.'herfindahl_index'(float): Herfindahl Index value, computed on portfolio weights.'gini_coefficient'(float): Gini Coefficient value, computed on portfolio weights.'absolute_max_weight'(float): Absolute maximum allocation for an asset.
Raises
PortfolioError: If weights have not been calculated via optimization.
Notes:
- All statistics are computed on absolute normalized weights (within the simplex), ensuring compatibility with long-short portfolios.
- This method is diagnostic only and does not modify portfolio weights.
- For meaningful interpretation, use these metrics in conjunction with risk and performance measures.