Source code for qf_lib.common.timeseries_analysis.risk_contribution_analysis

#     Copyright 2016-present CERN – European Organization for Nuclear Research
#
#     Licensed under the Apache License, Version 2.0 (the "License");
#     you may not use this file except in compliance with the License.
#     You may obtain a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#     Unless required by applicable law or agreed to in writing, software
#     distributed under the License is distributed on an "AS IS" BASIS,
#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#     See the License for the specific language governing permissions and
#     limitations under the License.

import numpy as np

from qf_lib.containers.dataframe.qf_dataframe import QFDataFrame
from qf_lib.containers.dataframe.simple_returns_dataframe import SimpleReturnsDataFrame
from qf_lib.containers.series.qf_series import QFSeries
from qf_lib.containers.series.simple_returns_series import SimpleReturnsSeries


[docs]class RiskContributionAnalysis: """ Calculates risk contribution metrics. """
[docs] @classmethod def get_risk_contribution(cls, factors_rets: SimpleReturnsDataFrame, weigths_of_assets: QFSeries, portfolio_rets: SimpleReturnsSeries) -> QFSeries: """ Calculates risk contribution of different factors to the portfolio. Risk is defined as volatility. Uses x-sigma-rho formula (MSCI Bara paper). Parameters ---------- factors_rets dataframe consisted of returns for different assets weigths_of_assets series of weights of each asset. It's indexed with names of assets. portfolio_rets return of the whole portfolio Returns ------- pandas.Series Series of risk contributions (one for each asset) to the portfolio. It's indexed with names of assets. """ volatility_of_returns = factors_rets.std(axis=0) correlation_asset_portfolio = factors_rets.apply(lambda series: series.corr(portfolio_rets)) risk_contribution = weigths_of_assets * volatility_of_returns * correlation_asset_portfolio normalized_risk_contribution_msci = risk_contribution / risk_contribution.sum() return normalized_risk_contribution_msci
[docs] @classmethod def get_risk_contribution_optimised( cls, assets_rets: SimpleReturnsDataFrame, weights_of_assets: QFSeries) -> QFSeries: """ Calculates risk contribution of each asset of the portfolio. Parameters ---------- assets_rets returns of assets building the portfolio (each assets in a separate column) weights_of_assets Series of weights (one for each asset). It's indexed with names of assets. Returns ------- Series of risk contributions (one for each asset). It's indexed with names of assets """ assets_covariance = assets_rets.cov() risk_contribution = cls._get_normalized_risk_contribution(assets_covariance, weights_of_assets) return risk_contribution
[docs] @classmethod def get_distance_to_equal_risk_contrib( cls, assets_returns_covariance: QFDataFrame, weights_of_assets: QFSeries) -> float: """ By minimising this function it is possible to calculate Equal Risk Contribution Portfolio. It has better numerical properties than simple approach ( riskContribution - mean(riskContribution) ) Details: http://www.thierry-roncalli.com/download/erc-slides.pdf Parameters ---------- assets_returns_covariance covariance matrix for assets returns data frame weights_of_assets weight of each asset in the portfolio. It's indexed with names of assets. Returns ------- L2 (euclidean) distance from equal risk distribution """ risk_contributions = cls._get_normalized_risk_contribution(assets_returns_covariance, weights_of_assets) num_of_assets = len(weights_of_assets) # sum up all the squared differences distance = 0 for i in range(0, num_of_assets - 1): for j in range(i + 1, num_of_assets): partial_diff = risk_contributions.iloc[i] - risk_contributions.iloc[j] distance += partial_diff ** 2 return np.sqrt(distance)
[docs] @classmethod def is_equal_risk_contribution(cls, returns_covariance: QFDataFrame, weights_of_assets: QFSeries) -> bool: """ Tells whether each asset has an equal risk contribution to the portfolio. Parameters ---------- returns_covariance DataFrame which is a covariance matrix. Columns and rows are both indexed with names of assets. weights_of_assets Series of weights (one weight for each asset). It's indexed with names of assets. Returns ------- True if each asset has and equal risk contribution. False -- otherwise. """ distance = cls.get_distance_to_equal_risk_contrib(returns_covariance, weights_of_assets) num_of_assets = len(weights_of_assets) distances_number = 0.5 * num_of_assets * (num_of_assets - 1) # number of pairs between different risk contribs # "reverse operation" to distance_to_equal_risk_contrib mean_risk_contribution_distance = distance / np.sqrt(distances_number) return mean_risk_contribution_distance < 0.005 # max distance: 0.5%
@classmethod def _get_normalized_risk_contribution( cls, assets_returns_covariance: QFDataFrame, weights_of_factors: QFSeries) -> QFSeries: raw_risk_contribution = weights_of_factors * (assets_returns_covariance.dot(weights_of_factors)) normalized_risk_contrib = raw_risk_contribution / raw_risk_contribution.sum() return normalized_risk_contrib