"""
The ``efficient_semivariance`` submodule houses the EfficientSemivariance class, which
generates portfolios along the mean-semivariance frontier.
"""
import cvxpy as cp
import numpy as np
from .. import objective_functions
from .efficient_frontier import EfficientFrontier
[文档]
class EfficientSemivariance(EfficientFrontier):
"""
EfficientSemivariance 对象帮助沿着均值-均值边界进行优化。这适合那些更关注下行偏差的用户。
可用变量:
- 输入:
- ``n_assets`` - int
- ``tickers`` - str list
- ``bounds`` - float tuple OR (float tuple) list
- ``returns`` - pd.DataFrame
- ``expected_returns`` - np.ndarray
- ``solver`` - str
- ``solver_options`` - {str: str} dict
- 输出: ``weights`` - np.ndarray
公共方法:
- ``min_semivariance()`` 最小化投资组合的 semivariance(下行偏差)。
- ``max_quadratic_utility()`` 在一定的风险规避下,最大化"下行二次方效用"。
- ``efficient_risk()`` 在给定的目标半偏差下,使收益最大化。
- ``efficient_return()`` 在给定的目标收益率下,最小化半偏差。
- ``add_objective()`` 为优化问题添加一个(凸)目标。
- ``add_constraint()`` 在优化问题中加入一个约束条件。
- ``convex_objective()`` 解决一个带有线性约束的通用凸目标。
- ``portfolio_performance()`` 计算优化后的投资组合期望收益率、半偏差和索提诺比率。
- ``set_weights()`` 从一个权重字典中创建 self.weights (np.ndarray)。
- ``clean_weights()`` 对权重进行四舍五入,并剪掉接近零的部分。
- ``save_weights_to_file()`` 将权重保存为 csv、json 或 txt。
"""
def __init__(
self,
expected_returns,
returns,
frequency=252,
benchmark=0,
weight_bounds=(0, 1),
solver=None,
verbose=False,
solver_options=None,
):
"""
:param expected_returns: expected returns for each asset. Can be None if
optimising for semideviation only.
:type expected_returns: pd.Series, list, np.ndarray
:param returns: (historic) returns for all your assets (no NaNs).
See ``expected_returns.returns_from_prices``.
:type returns: pd.DataFrame or np.array
:param frequency: number of time periods in a year, defaults to 252 (the number
of trading days in a year). This must agree with the frequency
parameter used in your ``expected_returns``.
:type frequency: int, optional
:param benchmark: the return threshold to distinguish "downside" and "upside".
This should match the frequency of your ``returns``,
i.e this should be a benchmark daily returns if your
``returns`` are also daily.
:param weight_bounds: minimum and maximum weight of each asset OR single min/max pair
if all identical, defaults to (0, 1). Must be changed to (-1, 1)
for portfolios with shorting.
:type weight_bounds: tuple OR tuple list, optional
:param solver: name of solver. list available solvers with: `cvxpy.installed_solvers()`
:type solver: str
:param verbose: whether performance and debugging info should be printed, defaults to False
:type verbose: bool, optional
:param solver_options: parameters for the given solver
:type solver_options: dict, optional
:raises TypeError: if ``expected_returns`` is not a series, list or array
"""
# Instantiate parent
super().__init__(
expected_returns=expected_returns,
cov_matrix=np.zeros((returns.shape[1],) * 2), # dummy
weight_bounds=weight_bounds,
solver=solver,
verbose=verbose,
solver_options=solver_options,
)
self.returns = self._validate_returns(returns)
self.benchmark = benchmark
self.frequency = frequency
self._T = self.returns.shape[0]
def min_volatility(self):
raise NotImplementedError("Please use min_semivariance instead.")
def max_sharpe(self, risk_free_rate=0.02):
raise NotImplementedError("Method not available in EfficientSemivariance")
[文档]
def min_semivariance(self, market_neutral=False):
"""
最小化投资组合的半方差(进一步解释见文档)。
:param market_neutral: whether the portfolio should be market neutral (weights sum to zero),
defaults to False. Requires negative lower weight bound.
:param market_neutral: bool, optional
:return: asset weights for the volatility-minimising portfolio
:rtype: OrderedDict
"""
p = cp.Variable(self._T, nonneg=True)
n = cp.Variable(self._T, nonneg=True)
self._objective = cp.sum(cp.square(n))
for obj in self._additional_objectives:
self._objective += obj
B = (self.returns.values - self.benchmark) / np.sqrt(self._T)
self.add_constraint(lambda w: B @ w - p + n == 0)
self._make_weight_sum_constraint(market_neutral)
return self._solve_cvxpy_opt_problem()
[文档]
def max_quadratic_utility(self, risk_aversion=1, market_neutral=False):
"""
最大化给定的二次效用,使用半方差而不是方差。
:param risk_aversion: risk aversion parameter (must be greater than 0),
defaults to 1
:type risk_aversion: positive float
:param market_neutral: whether the portfolio should be market neutral (weights sum to zero),
defaults to False. Requires negative lower weight bound.
:param market_neutral: bool, optional
:return: asset weights for the maximum-utility portfolio
:rtype: OrderedDict
"""
if risk_aversion <= 0:
raise ValueError("risk aversion coefficient must be greater than zero")
update_existing_parameter = self.is_parameter_defined("risk_aversion")
if update_existing_parameter:
self._validate_market_neutral(market_neutral)
self.update_parameter_value("risk_aversion", risk_aversion)
else:
p = cp.Variable(self._T, nonneg=True)
n = cp.Variable(self._T, nonneg=True)
mu = objective_functions.portfolio_return(self._w, self.expected_returns)
mu /= self.frequency
risk_aversion_par = cp.Parameter(
value=risk_aversion, name="risk_aversion", nonneg=True
)
self._objective = mu + 0.5 * risk_aversion_par * cp.sum(cp.square(n))
for obj in self._additional_objectives:
self._objective += obj
B = (self.returns.values - self.benchmark) / np.sqrt(self._T)
self.add_constraint(lambda w: B @ w - p + n == 0)
self._make_weight_sum_constraint(market_neutral)
return self._solve_cvxpy_opt_problem()
[文档]
def efficient_risk(self, target_semideviation, market_neutral=False):
"""
使目标半标准差(下行标准偏差)的收益最大化。由此产生的投资组合将有一个小于目标的半标准差(但不保证相等)。
:param target_semideviation: the desired maximum semideviation of the resulting portfolio.
:type target_semideviation: float
:param market_neutral: whether the portfolio should be market neutral (weights sum to zero),
defaults to False. Requires negative lower weight bound.
:param market_neutral: bool, optional
:return: asset weights for the efficient risk portfolio
:rtype: OrderedDict
"""
update_existing_parameter = self.is_parameter_defined("target_semivariance")
if update_existing_parameter:
self._validate_market_neutral(market_neutral)
self.update_parameter_value(
"target_semivariance", target_semideviation**2
)
else:
self._objective = objective_functions.portfolio_return(
self._w, self.expected_returns
)
for obj in self._additional_objectives:
self._objective += obj
p = cp.Variable(self._T, nonneg=True)
n = cp.Variable(self._T, nonneg=True)
target_semivariance = cp.Parameter(
value=target_semideviation**2, name="target_semivariance", nonneg=True
)
self.add_constraint(
lambda _: self.frequency * cp.sum(cp.square(n)) <= target_semivariance
)
B = (self.returns.values - self.benchmark) / np.sqrt(self._T)
self.add_constraint(lambda w: B @ w - p + n == 0)
self._make_weight_sum_constraint(market_neutral)
return self._solve_cvxpy_opt_problem()
[文档]
def efficient_return(self, target_return, market_neutral=False):
"""
在给定的目标收益下,使半方差最小。
:param target_return: the desired return of the resulting portfolio.
:type target_return: float
:param market_neutral: whether the portfolio should be market neutral (weights sum to zero),
defaults to False. Requires negative lower weight bound.
:type market_neutral: bool, optional
:raises ValueError: if ``target_return`` is not a positive float
:raises ValueError: if no portfolio can be found with return equal to ``target_return``
:return: asset weights for the optimal portfolio
:rtype: OrderedDict
"""
if not isinstance(target_return, float) or target_return < 0:
raise ValueError("target_return should be a positive float")
if target_return > np.abs(self.expected_returns).max():
raise ValueError(
"target_return must be lower than the largest expected return"
)
update_existing_parameter = self.is_parameter_defined("target_return")
if update_existing_parameter:
self._validate_market_neutral(market_neutral)
self.update_parameter_value("target_return", target_return)
else:
p = cp.Variable(self._T, nonneg=True)
n = cp.Variable(self._T, nonneg=True)
self._objective = cp.sum(cp.square(n))
for obj in self._additional_objectives:
self._objective += obj
target_return_par = cp.Parameter(name="target_return", value=target_return)
self.add_constraint(
lambda w: cp.sum(w @ self.expected_returns) >= target_return_par
)
B = (self.returns.values - self.benchmark) / np.sqrt(self._T)
self.add_constraint(lambda w: B @ w - p + n == 0)
self._make_weight_sum_constraint(market_neutral)
return self._solve_cvxpy_opt_problem()