11"""
2- The ``efficient_cdar`` submodule houses the EfficientCDaR class, which
3- generates portfolios along the mean-CDaR (conditional drawdown-at-risk) frontier.
2+ The ``efficient_cdar`` submodule houses the EfficientCDaR class.
3+
4+ This module generates portfolios along the mean-CDaR
5+ (conditional drawdown-at-risk) frontier.
46"""
57
68import warnings
1416
1517class EfficientCDaR (EfficientFrontier ):
1618 """
17- The EfficientCDaR class allows for optimisation along the mean-CDaR frontier, using the
18- formulation of Chekhlov, Ursayev and Zabarankin (2005).
19-
20- Instance variables:
21-
22- - Inputs:
23-
24- - ``n_assets`` - int
25- - ``tickers`` - str list
26- - ``bounds`` - float tuple OR (float tuple) list
27- - ``returns`` - pd.DataFrame
28- - ``expected_returns`` - np.ndarray
29- - ``solver`` - str
30- - ``solver_options`` - {str: str} dict
31-
32- - Output: ``weights`` - np.ndarray
33-
34- Public methods:
35-
36- - ``min_cdar()`` minimises the CDaR
37- - ``efficient_risk()`` maximises return for a given CDaR
38- - ``efficient_return()`` minimises CDaR for a given target return
39- - ``add_objective()`` adds a (convex) objective to the optimisation problem
40- - ``add_constraint()`` adds a (linear) constraint to the optimisation problem
41-
42- - ``portfolio_performance()`` calculates the expected return and CDaR of the portfolio
43- - ``set_weights()`` creates self.weights (np.ndarray) from a weights dict
44- - ``clean_weights()`` rounds the weights and clips near-zeros.
45- - ``save_weights_to_file()`` saves the weights to csv, json, or txt.
19+ Optimization along the mean-CDaR (Conditional Drawdown at Risk) frontier.
20+
21+ The EfficientCDaR class allows for optimisation along the mean-CDaR
22+ frontier, using the formulation of Chekhlov, Ursayev and Zabarankin (2005).
23+
24+ Attributes
25+ ----------
26+ n_assets : int
27+ Number of assets.
28+ tickers : list
29+ List of asset tickers.
30+ bounds : tuple or list
31+ Weight bounds for each asset.
32+ returns : pd.DataFrame
33+ Historical returns data.
34+ expected_returns : np.ndarray
35+ Expected returns for each asset.
36+ solver : str
37+ CVXPY solver name.
38+ solver_options : dict
39+ Solver parameters.
40+ weights : np.ndarray
41+ Optimized portfolio weights.
42+
43+ Examples
44+ --------
45+ >>> from pypfopt import EfficientCDaR, expected_returns
46+ >>> # mu = expected_returns.mean_historical_return(prices)
47+ >>> # returns = expected_returns.returns_from_prices(prices)
48+ >>> # ef = EfficientCDaR(mu, returns)
49+ >>> # weights = ef.min_cdar()
4650 """
4751
4852 def __init__ (
@@ -56,24 +60,35 @@ def __init__(
5660 solver_options = None ,
5761 ):
5862 """
59- :param expected_returns: expected returns for each asset. Can be None if
60- optimising for CDaR only.
61- :type expected_returns: pd.Series, list, np.ndarray
62- :param returns: (historic) returns for all your assets (no NaNs).
63- See ``expected_returns.returns_from_prices``.
64- :type returns: pd.DataFrame or np.array
65- :param beta: confidence level, defaults to 0.95 (i.e expected drawdown on the worst (1-beta) days).
66- :param weight_bounds: minimum and maximum weight of each asset OR single min/max pair
67- if all identical, defaults to (0, 1). Must be changed to (-1, 1)
68- for portfolios with shorting.
69- :type weight_bounds: tuple OR tuple list, optional
70- :param solver: name of solver. list available solvers with: `cvxpy.installed_solvers()`
71- :type solver: str
72- :param verbose: whether performance and debugging info should be printed, defaults to False
73- :type verbose: bool, optional
74- :param solver_options: parameters for the given solver
75- :type solver_options: dict, optional
76- :raises TypeError: if ``expected_returns`` is not a series, list or array
63+ Initialize the EfficientCDaR object.
64+
65+ Parameters
66+ ----------
67+ expected_returns : pd.Series, list, or np.ndarray
68+ Expected returns for each asset. Can be None if
69+ optimising for CDaR only.
70+ returns : pd.DataFrame or np.ndarray
71+ (Historic) returns for all your assets (no NaNs).
72+ See ``expected_returns.returns_from_prices``.
73+ beta : float, optional
74+ Confidence level, defaults to 0.95 (i.e expected drawdown
75+ on the worst (1-beta) days).
76+ weight_bounds : tuple or list, optional
77+ Minimum and maximum weight of each asset OR single min/max pair
78+ if all identical, defaults to (0, 1). Must be changed to (-1, 1)
79+ for portfolios with shorting.
80+ solver : str, optional
81+ Name of solver. List available solvers with ``cvxpy.installed_solvers()``.
82+ verbose : bool, optional
83+ Whether performance and debugging info should be printed,
84+ defaults to False.
85+ solver_options : dict, optional
86+ Parameters for the given solver.
87+
88+ Raises
89+ ------
90+ TypeError
91+ If ``expected_returns`` is not a series, list or array.
7792 """
7893 super ().__init__ (
7994 expected_returns = expected_returns ,
@@ -91,10 +106,36 @@ def __init__(
91106 self ._z = cp .Variable (len (self .returns ))
92107
93108 def set_weights (self , input_weights ):
109+ """
110+ Override parent method.
111+
112+ Raises
113+ ------
114+ NotImplementedError
115+ Always, as set_weights is not available in EfficientCDaR.
116+ """
94117 raise NotImplementedError ("Method not available in EfficientCDaR." )
95118
96119 @staticmethod
97120 def _validate_beta (beta ):
121+ """
122+ Validate the beta (confidence level) parameter.
123+
124+ Parameters
125+ ----------
126+ beta : float
127+ Confidence level.
128+
129+ Returns
130+ -------
131+ float
132+ Validated beta value.
133+
134+ Raises
135+ ------
136+ ValueError
137+ If beta is not between 0 and 1.
138+ """
98139 if not (0 <= beta < 1 ):
99140 raise ValueError ("beta must be between 0 and 1" )
100141 if beta <= 0.2 :
@@ -105,23 +146,59 @@ def _validate_beta(beta):
105146 return beta
106147
107148 def min_volatility (self ):
149+ """
150+ Override parent method.
151+
152+ Raises
153+ ------
154+ NotImplementedError
155+ Always, as min_volatility is not available in EfficientCDaR.
156+ Use min_cdar instead.
157+ """
108158 raise NotImplementedError ("Please use min_cdar instead." )
109159
110160 def max_sharpe (self , risk_free_rate = 0.0 ):
161+ """
162+ Override parent method.
163+
164+ Raises
165+ ------
166+ NotImplementedError
167+ Always, as max_sharpe is not available in EfficientCDaR.
168+ """
111169 raise NotImplementedError ("Method not available in EfficientCDaR." )
112170
113171 def max_quadratic_utility (self , risk_aversion = 1 , market_neutral = False ):
172+ """
173+ Override parent method.
174+
175+ Raises
176+ ------
177+ NotImplementedError
178+ Always, as max_quadratic_utility is not available in EfficientCDaR.
179+ """
114180 raise NotImplementedError ("Method not available in EfficientCDaR." )
115181
116182 def min_cdar (self , market_neutral = False ):
117183 """
118- Minimise portfolio CDaR (see docs for further explanation).
119-
120- :param market_neutral: whether the portfolio should be market neutral (weights sum to zero),
121- defaults to False. Requires negative lower weight bound.
122- :param market_neutral: bool, optional
123- :return: asset weights for the volatility-minimising portfolio
124- :rtype: OrderedDict
184+ Minimise portfolio CDaR (Conditional Drawdown at Risk).
185+
186+ Parameters
187+ ----------
188+ market_neutral : bool, optional
189+ Whether the portfolio should be market neutral (weights sum to zero),
190+ defaults to False. Requires negative lower weight bound.
191+
192+ Returns
193+ -------
194+ OrderedDict
195+ Asset weights for the CDaR-minimising portfolio.
196+
197+ Examples
198+ --------
199+ >>> from pypfopt import EfficientCDaR
200+ >>> # ef = EfficientCDaR(mu, returns)
201+ >>> # weights = ef.min_cdar()
125202 """
126203 self ._objective = self ._alpha + 1.0 / (
127204 len (self .returns ) * (1 - self ._beta )
@@ -138,15 +215,30 @@ def efficient_return(self, target_return, market_neutral=False):
138215 """
139216 Minimise CDaR for a given target return.
140217
141- :param target_return: the desired return of the resulting portfolio.
142- :type target_return: float
143- :param market_neutral: whether the portfolio should be market neutral (weights sum to zero),
144- defaults to False. Requires negative lower weight bound.
145- :type market_neutral: bool, optional
146- :raises ValueError: if ``target_return`` is not a positive float
147- :raises ValueError: if no portfolio can be found with return equal to ``target_return``
148- :return: asset weights for the optimal portfolio
149- :rtype: OrderedDict
218+ Parameters
219+ ----------
220+ target_return : float
221+ The desired return of the resulting portfolio.
222+ market_neutral : bool, optional
223+ Whether the portfolio should be market neutral (weights sum to zero),
224+ defaults to False. Requires negative lower weight bound.
225+
226+ Returns
227+ -------
228+ OrderedDict
229+ Asset weights for the optimal portfolio.
230+
231+ Raises
232+ ------
233+ ValueError
234+ If ``target_return`` is not a positive float.
235+ If no portfolio can be found with return equal to ``target_return``.
236+
237+ Examples
238+ --------
239+ >>> from pypfopt import EfficientCDaR
240+ >>> # ef = EfficientCDaR(mu, returns)
241+ >>> # weights = ef.efficient_return(target_return=0.15)
150242 """
151243
152244 update_existing_parameter = self .is_parameter_defined ("target_return" )
@@ -165,16 +257,28 @@ def efficient_return(self, target_return, market_neutral=False):
165257 def efficient_risk (self , target_cdar , market_neutral = False ):
166258 """
167259 Maximise return for a target CDaR.
260+
168261 The resulting portfolio will have a CDaR less than the target
169262 (but not guaranteed to be equal).
170263
171- :param target_cdar: the desired maximum CDaR of the resulting portfolio.
172- :type target_cdar: float
173- :param market_neutral: whether the portfolio should be market neutral (weights sum to zero),
174- defaults to False. Requires negative lower weight bound.
175- :param market_neutral: bool, optional
176- :return: asset weights for the efficient risk portfolio
177- :rtype: OrderedDict
264+ Parameters
265+ ----------
266+ target_cdar : float
267+ The desired maximum CDaR of the resulting portfolio.
268+ market_neutral : bool, optional
269+ Whether the portfolio should be market neutral (weights sum to zero),
270+ defaults to False. Requires negative lower weight bound.
271+
272+ Returns
273+ -------
274+ OrderedDict
275+ Asset weights for the efficient risk portfolio.
276+
277+ Examples
278+ --------
279+ >>> from pypfopt import EfficientCDaR
280+ >>> # ef = EfficientCDaR(mu, returns)
281+ >>> # weights = ef.efficient_risk(target_cdar=0.10)
178282 """
179283
180284 update_existing_parameter = self .is_parameter_defined ("target_cdar" )
@@ -202,6 +306,12 @@ def efficient_risk(self, target_cdar, market_neutral=False):
202306 return self ._solve_cvxpy_opt_problem ()
203307
204308 def _add_cdar_constraints (self ) -> None :
309+ """
310+ Add the CDaR-specific constraints to the optimization problem.
311+
312+ This method adds the constraints required for CDaR optimization
313+ to the CVXPY problem.
314+ """
205315 self .add_constraint (lambda _ : self ._z >= self ._u [1 :] - self ._alpha )
206316 self .add_constraint (
207317 lambda w : self ._u [1 :] >= self ._u [:- 1 ] - self .returns .values @ w
@@ -212,14 +322,32 @@ def _add_cdar_constraints(self) -> None:
212322
213323 def portfolio_performance (self , verbose = False ):
214324 """
215- After optimising, calculate (and optionally print) the performance of the optimal
216- portfolio, specifically: expected return, CDaR
217-
218- :param verbose: whether performance should be printed, defaults to False
219- :type verbose: bool, optional
220- :raises ValueError: if weights have not been calculated yet
221- :return: expected return, CDaR.
222- :rtype: (float, float)
325+ Calculate the performance of the optimal portfolio.
326+
327+ After optimising, calculate (and optionally print) the performance
328+ of the optimal portfolio, specifically: expected return, CDaR.
329+
330+ Parameters
331+ ----------
332+ verbose : bool, optional
333+ Whether performance should be printed, defaults to False.
334+
335+ Returns
336+ -------
337+ tuple
338+ A tuple of (expected return, CDaR).
339+
340+ Raises
341+ ------
342+ ValueError
343+ If weights have not been calculated yet.
344+
345+ Examples
346+ --------
347+ >>> from pypfopt import EfficientCDaR
348+ >>> # ef = EfficientCDaR(mu, returns)
349+ >>> # ef.min_cdar()
350+ >>> # mu, cdar = ef.portfolio_performance(verbose=True)
223351 """
224352 mu = objective_functions .portfolio_return (
225353 self .weights , self .expected_returns , negative = False
0 commit comments