pypfopt.black_litterman 源代码

"""
``black_litterman`` 模块包含 BlackLittermanModel 类,它在给定先验估计值和用户提供的观点的情况下,生成期望收益的后验估计值。
此外,还定义了两个效用函数,它们计算:

- 市场隐含的收益的先验估计
- 市场隐含的风险规避参数
"""
import sys
import warnings

import numpy as np
import pandas as pd

from . import base_optimizer


[文档] def market_implied_prior_returns( market_caps, risk_aversion, cov_matrix, risk_free_rate=0.02 ): r""" 计算市场权重所隐含的收益的先验估计。换句话说,鉴于每种资产对市场组合风险的贡献,我们期望得到多少补偿? .. math:: \Pi = \delta \Sigma w_{mkt} :param market_caps: market capitalisations of all assets :type market_caps: {ticker: cap} dict or pd.Series :param risk_aversion: risk aversion parameter :type risk_aversion: positive float :param cov_matrix: covariance matrix of asset returns :type cov_matrix: pd.DataFrame :param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02. You should use the appropriate time period, corresponding to the covariance matrix. :type risk_free_rate: float, optional :return: prior estimate of returns as implied by the market caps :rtype: pd.Series """ if not isinstance(cov_matrix, pd.DataFrame): warnings.warn( "If cov_matrix is not a dataframe, market cap index must be aligned to cov_matrix", RuntimeWarning, ) mcaps = pd.Series(market_caps) mkt_weights = mcaps / mcaps.sum() # Pi is excess returns so must add risk_free_rate to get return. return risk_aversion * cov_matrix.dot(mkt_weights) + risk_free_rate
[文档] def market_implied_risk_aversion(market_prices, frequency=252, risk_free_rate=0.02): r""" 根据市场价格计算市场暗示的风险规避参数(即风险的市场价格)。 例如,如果市场每年的超额收益为 10%,方差为 5%,则风险规避参数为 2,即你必须得到 2 倍的方差补偿。 .. math:: \delta = \frac{R - R_f}{\sigma^2} :param market_prices: the (daily) prices of the market portfolio, e.g SPY. :type market_prices: pd.Series with DatetimeIndex. :param frequency: number of time periods in a year, defaults to 252 (the number of trading days in a year) :type frequency: int, optional :param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02. The period of the risk-free rate should correspond to the frequency of expected returns. :type risk_free_rate: float, optional :raises TypeError: if market_prices cannot be parsed :return: market-implied risk aversion :rtype: float """ if not isinstance(market_prices, (pd.Series, pd.DataFrame)): raise TypeError("Please format market_prices as a pd.Series") rets = market_prices.pct_change().dropna() r = rets.mean() * frequency var = rets.var() * frequency return (r - risk_free_rate) / var
[文档] class BlackLittermanModel(base_optimizer.BaseOptimizer): """ BlackLittermanModel 对象(继承自 BaseOptimizer)包含需要一个特定的输入格式, 指定先验、观点、观点中的不确定性,以及将观点映射到资产范围的拾取矩阵。 然后我们可以计算收益和协方差的后验估计值。在可能的情况下,我们提供了帮助方法来提供默认值。 可用变量: - 输入: - ``cov_matrix`` - np.ndarray - ``n_assets`` - int - ``tickers`` - str list - ``Q`` - np.ndarray - ``P`` - np.ndarray - ``pi`` - np.ndarray - ``omega`` - np.ndarray - ``tau`` - float - 输出: - ``posterior_rets`` - pd.Series - ``posterior_cov`` - pd.DataFrame - ``weights`` - np.ndarray 公共方法: - ``default_omega()`` 观点不确定性与资产方差成比例 - ``idzorek_method()`` 将指定为百分比的观点转换为 BL 不确定度 - ``bl_returns()`` 收益的后验估计值 - ``bl_cov()`` 协方差的后验估计值 - ``bl_weights()`` 后验收益所隐含的权重 - ``portfolio_performance()`` 计算投资组合的期望收益率、波动率和夏普比率。 - ``set_weights()`` 从权重数据中创建 self.weights (np.ndarray)。 - ``clean_weights()`` 对权重进行四舍五入,并剪掉接近零的部分。 - ``save_weights_to_file()`` 将权重保存为 csv、json 或 txt。 """
[文档] def __init__( self, cov_matrix, pi=None, absolute_views=None, Q=None, P=None, omega=None, view_confidences=None, tau=0.05, risk_aversion=1, **kwargs ): """ :param cov_matrix: NxN covariance matrix of returns :type cov_matrix: pd.DataFrame or np.ndarray :param pi: Nx1 prior estimate of returns, defaults to None. If pi="market", calculate a market-implied prior (requires market_caps to be passed). If pi="equal", use an equal-weighted prior. :type pi: np.ndarray, pd.Series, optional :param absolute_views: a collection of K absolute views on a subset of assets, defaults to None. If this is provided, we do not need P, Q. :type absolute_views: pd.Series or dict, optional :param Q: Kx1 views vector, defaults to None :type Q: np.ndarray or pd.DataFrame, optional :param P: KxN picking matrix, defaults to None :type P: np.ndarray or pd.DataFrame, optional :param omega: KxK view uncertainty matrix (diagonal), defaults to None Can instead pass "idzorek" to use Idzorek's method (requires you to pass view_confidences). If omega="default" or None, we set the uncertainty proportional to the variance. :type omega: np.ndarray or Pd.DataFrame, or string, optional :param view_confidences: Kx1 vector of percentage view confidences (between 0 and 1), required to compute omega via Idzorek's method. :type view_confidences: np.ndarray, pd.Series, list, optional :param tau: the weight-on-views scalar (default is 0.05) :type tau: float, optional :param risk_aversion: risk aversion parameter, defaults to 1 :type risk_aversion: positive float, optional :param market_caps: (kwarg) market caps for the assets, required if pi="market" :type market_caps: np.ndarray, pd.Series, optional :param risk_free_rate: (kwarg) risk_free_rate is needed in some methods :type risk_free_rate: float, defaults to 0.02 """ if sys.version_info[1] == 5: # pragma: no cover warnings.warn( "When using python 3.5 you must explicitly construct the Black-Litterman inputs" ) # Keep raw dataframes self._raw_cov_matrix = cov_matrix #  Initialise base optimizer if isinstance(cov_matrix, np.ndarray): self.cov_matrix = cov_matrix super().__init__(len(cov_matrix), list(range(len(cov_matrix)))) else: self.cov_matrix = cov_matrix.values super().__init__(len(cov_matrix), cov_matrix.columns) #  Sanitise inputs if absolute_views is not None: self.Q, self.P = self._parse_views(absolute_views) else: self._set_Q_P(Q, P) self._set_risk_aversion(risk_aversion) self._set_pi(pi, **kwargs) self._set_tau(tau) # Make sure all dimensions work self._check_attribute_dimensions() self._set_omega(omega, view_confidences) # Private intermediaries self._tau_sigma_P = None self._A = None self.posterior_rets = None self.posterior_cov = None
def _parse_views(self, absolute_views): """ Given a collection (dict or series) of absolute views, construct the appropriate views vector and picking matrix. The views must be a subset of the tickers in the covariance matrix. {"AAPL": 0.20, "GOOG": 0.12, "XOM": -0.30} :param absolute_views: absolute views on asset performances :type absolute_views: dict, pd.Series """ if not isinstance(absolute_views, (dict, pd.Series)): raise TypeError("views should be a dict or pd.Series") # Coerce to series views = pd.Series(absolute_views) k = len(views) Q = np.zeros((k, 1)) P = np.zeros((k, self.n_assets)) for i, view_ticker in enumerate(views.keys()): try: Q[i] = views[view_ticker] P[i, list(self.tickers).index(view_ticker)] = 1 except ValueError: #  Could make this smarter by just skipping raise ValueError("Providing a view on an asset not in the universe") return Q, P def _set_Q_P(self, Q, P): if isinstance(Q, (pd.Series, pd.DataFrame)): self.Q = Q.values.reshape(-1, 1) elif isinstance(Q, np.ndarray): self.Q = Q.reshape(-1, 1) else: raise TypeError("Q must be an array or dataframe") if isinstance(P, pd.DataFrame): self.P = P.values elif isinstance(P, np.ndarray): self.P = P elif len(self.Q) == self.n_assets: # If a view on every asset is provided, P defaults # to the identity matrix. self.P = np.eye(self.n_assets) else: raise TypeError("P must be an array or dataframe") def _set_pi(self, pi, **kwargs): if pi is None: warnings.warn("Running Black-Litterman with no prior.") self.pi = np.zeros((self.n_assets, 1)) elif isinstance(pi, (pd.Series, pd.DataFrame)): self.pi = pi.values.reshape(-1, 1) elif isinstance(pi, np.ndarray): self.pi = pi.reshape(-1, 1) elif pi == "market": if "market_caps" not in kwargs: raise ValueError( "Please pass a series/array of market caps via the market_caps keyword argument" ) market_caps = kwargs.get("market_caps") risk_free_rate = kwargs.get("risk_free_rate", 0) market_prior = market_implied_prior_returns( market_caps, self.risk_aversion, self._raw_cov_matrix, risk_free_rate ) self.pi = market_prior.values.reshape(-1, 1) elif pi == "equal": self.pi = np.ones((self.n_assets, 1)) / self.n_assets else: raise TypeError("pi must be an array or series") def _set_tau(self, tau): if tau <= 0 or tau > 1: raise ValueError("tau should be between 0 and 1") self.tau = tau def _set_risk_aversion(self, risk_aversion): if risk_aversion <= 0: raise ValueError("risk_aversion should be a positive float") self.risk_aversion = risk_aversion def _set_omega(self, omega, view_confidences): if isinstance(omega, pd.DataFrame): self.omega = omega.values elif isinstance(omega, np.ndarray): self.omega = omega elif omega == "idzorek": if view_confidences is None: raise ValueError( "To use Idzorek's method, please supply a vector of percentage " "confidence levels for each view." ) if not isinstance(view_confidences, np.ndarray): try: view_confidences = np.array(view_confidences).reshape(-1, 1) assert view_confidences.shape[0] == self.Q.shape[0] assert np.issubdtype(view_confidences.dtype, np.number) except AssertionError: raise ValueError( "view_confidences should be a numpy 1D array or vector with the same length " "as the number of views." ) self.omega = BlackLittermanModel.idzorek_method( view_confidences, self.cov_matrix, self.pi, self.Q, self.P, self.tau, self.risk_aversion, ) elif omega is None or omega == "default": self.omega = BlackLittermanModel.default_omega( self.cov_matrix, self.P, self.tau ) else: raise TypeError("self.omega must be a square array, dataframe, or string") K = len(self.Q) assert self.omega.shape == (K, K), "omega must have dimensions KxK" def _check_attribute_dimensions(self): """ Helper method to ensure that all of the attributes created by the initialiser have the correct dimensions, to avoid linear algebra errors later on. :raises ValueError: if there are incorrect dimensions. """ N = self.n_assets K = len(self.Q) assert self.pi.shape == (N, 1), "pi must have dimensions Nx1" assert self.P.shape == (K, N), "P must have dimensions KxN" assert self.cov_matrix.shape == (N, N), "cov_matrix must have shape NxN"
[文档] @staticmethod def default_omega(cov_matrix, P, tau): """ 如果没有提供不确定性矩阵 omega,我们使用 He and Litterman(1999)的方法进行计算,这样 omega/tau 的比率与观点组合的方差成比例。 :return: KxK diagonal uncertainty matrix :rtype: np.ndarray """ return np.diag(np.diag(tau * P @ cov_matrix @ P.T))
[文档] @staticmethod def idzorek_method(view_confidences, cov_matrix, pi, Q, P, tau, risk_aversion=1): """ 使用 Idzorek 的方法来创建给定用户指定的置信度百分比的不确定性矩阵。 我们使用 Jay Walters 在《The Black-Litterman Model in Detail》(2014)中描述的闭合式解决方案。 :param view_confidences: Kx1 vector of percentage view confidences (between 0 and 1), required to compute omega via Idzorek's method. :type view_confidences: np.ndarray, pd.Series, list,, optional :return: KxK diagonal uncertainty matrix :rtype: np.ndarray """ view_omegas = [] for view_idx in range(len(Q)): conf = view_confidences[view_idx] if conf < 0 or conf > 1: raise ValueError("View confidences must be between 0 and 1") # Special handler to avoid dividing by zero. # If zero conf, return very big number as uncertainty if conf == 0: view_omegas.append(1e6) continue P_view = P[view_idx].reshape(1, -1) alpha = (1 - conf) / conf # formula (44) omega = tau * alpha * P_view @ cov_matrix @ P_view.T # formula (41) view_omegas.append(omega.item()) return np.diag(view_omegas)
[文档] def bl_returns(self): """ 鉴于对某些资产的看法,计算收益向量的后验估计。 :return: posterior returns vector :rtype: pd.Series """ if self._tau_sigma_P is None: self._tau_sigma_P = self.tau * self.cov_matrix @ self.P.T # Solve the linear system Ax = b to avoid inversion if self._A is None: self._A = (self.P @ self._tau_sigma_P) + self.omega b = self.Q - self.P @ self.pi post_rets = self.pi + self._tau_sigma_P @ np.linalg.solve(self._A, b) return pd.Series(post_rets.flatten(), index=self.tickers)
[文档] def bl_cov(self): """ 考虑到对某些资产的看法,计算协方差矩阵的后验估计。基于 He and Litterman(2002)。 假设 omega 是对角线。如果不是这样,请手动设置 omega_inv。 :return: posterior covariance matrix :rtype: pd.DataFrame """ if self._tau_sigma_P is None: self._tau_sigma_P = self.tau * self.cov_matrix @ self.P.T if self._A is None: self._A = (self.P @ self._tau_sigma_P) + self.omega b = self._tau_sigma_P.T M = self.tau * self.cov_matrix - self._tau_sigma_P @ np.linalg.solve(self._A, b) posterior_cov = self.cov_matrix + M return pd.DataFrame(posterior_cov, index=self.tickers, columns=self.tickers)
[文档] def bl_weights(self, risk_aversion=None): r""" 考虑到风险的市场价格,计算后验收益所隐含的权重。从技术上讲,这可以适用于任何期望收益的估计,实际上是均值-方差优化的一个特例。 .. math:: w = (\delta \Sigma)^{-1} E(R) :param risk_aversion: risk aversion parameter, defaults to 1 :type risk_aversion: positive float, optional :return: asset weights implied by returns :rtype: OrderedDict """ if risk_aversion is None: risk_aversion = self.risk_aversion self.posterior_rets = self.bl_returns() A = risk_aversion * self.cov_matrix b = self.posterior_rets raw_weights = np.linalg.solve(A, b) self.weights = raw_weights / raw_weights.sum() return self._make_output_weights()
[文档] def optimize(self, risk_aversion=None): """ bl_weights 的别名,以便与其他方法保持一致。 """ return self.bl_weights(risk_aversion)
[文档] def portfolio_performance(self, verbose=False, risk_free_rate=0.02): """ 优化后,计算(并可选择打印)最优投资组合的表现。目前计算的是期望收益率、波动率和夏普比率。该方法使用 BL 后验收益和协方差矩阵。 :param verbose: whether performance should be printed, defaults to False :type verbose: bool, optional :param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02. The period of the risk-free rate should correspond to the frequency of expected returns. :type risk_free_rate: float, optional :raises ValueError: if weights have not been calculated yet :return: expected return, volatility, Sharpe ratio. :rtype: (float, float, float) """ if self.posterior_cov is None: self.posterior_cov = self.bl_cov() return base_optimizer.portfolio_performance( self.weights, self.posterior_rets, self.posterior_cov, verbose, risk_free_rate, )