From ad6ebb8043400cd6b09af39755c32696112eb916 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 00:22:16 +0000 Subject: [PATCH 1/5] Initial plan From 77e6151fb66909d3401d61bbe362c0897ee541c3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 00:41:12 +0000 Subject: [PATCH 2/5] Convert all pypfopt docstrings from Sphinx/RST to numpydoc format Co-authored-by: fkiraly <7985502+fkiraly@users.noreply.github.com> --- pypfopt/base_optimizer.py | 227 ++++++++----- pypfopt/black_litterman.py | 204 +++++++----- pypfopt/cla.py | 115 ++++--- pypfopt/discrete_allocation.py | 111 ++++--- pypfopt/efficient_frontier/efficient_cdar.py | 124 +++++--- pypfopt/efficient_frontier/efficient_cvar.py | 124 +++++--- .../efficient_frontier/efficient_frontier.py | 185 +++++++---- .../efficient_semivariance.py | 167 ++++++---- pypfopt/expected_returns.py | 189 ++++++----- pypfopt/hierarchical_portfolio.py | 109 ++++--- pypfopt/objective_functions.py | 199 +++++++----- pypfopt/plotting.py | 134 ++++---- pypfopt/risk_models.py | 299 +++++++++++------- weights.csv | 20 ++ 14 files changed, 1394 insertions(+), 813 deletions(-) create mode 100644 weights.csv diff --git a/pypfopt/base_optimizer.py b/pypfopt/base_optimizer.py index f988409c..49de6624 100644 --- a/pypfopt/base_optimizer.py +++ b/pypfopt/base_optimizer.py @@ -39,10 +39,12 @@ class BaseOptimizer: def __init__(self, n_assets, tickers=None): """ - :param n_assets: number of assets - :type n_assets: int - :param tickers: name of assets - :type tickers: list + Parameters + ---------- + n_assets : int + number of assets + tickers : list + name of assets """ self.n_assets = n_assets if tickers is None: @@ -71,8 +73,10 @@ def set_weights(self, input_weights): """ Utility function to set weights attribute (np.array) from user input - :param input_weights: {ticker: weight} dict - :type input_weights: dict + Parameters + ---------- + input_weights : dict + {ticker: weight} dict """ self.weights = np.array([input_weights[ticker] for ticker in self.tickers]) @@ -81,13 +85,18 @@ def clean_weights(self, cutoff=1e-4, rounding=5): Helper method to clean the raw weights, setting any weights whose absolute values are below the cutoff to zero, and rounding the rest. - :param cutoff: the lower bound, defaults to 1e-4 - :type cutoff: float, optional - :param rounding: number of decimal places to round the weights, defaults to 5. - Set to None if rounding is not desired. - :type rounding: int, optional - :return: asset weights - :rtype: OrderedDict + Parameters + ---------- + cutoff : float, optional + the lower bound, defaults to 1e-4 + rounding : int, optional + number of decimal places to round the weights, defaults to 5. + Set to None if rounding is not desired. + + Returns + ------- + OrderedDict + asset weights """ if self.weights is None: raise AttributeError("Weights not yet computed") @@ -104,8 +113,10 @@ def save_weights_to_file(self, filename="weights.csv"): """ Utility method to save weights to a text file. - :param filename: name of file. Should be csv, json, or txt. - :type filename: str + Parameters + ---------- + filename : str + name of file. Should be csv, json, or txt. """ clean_weights = self.clean_weights() @@ -160,16 +171,18 @@ def __init__( solver_options=None, ): """ - :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, optional. - :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 + Parameters + ---------- + weight_bounds : tuple or list of tuples, optional + 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. + solver : str, optional + name of solver. list available solvers with: ``cvxpy.installed_solvers()`` + verbose : bool, optional + whether performance and debugging info should be printed, defaults to False + solver_options : dict, optional + parameters for the given solver """ super().__init__(n_assets, tickers) @@ -204,12 +217,21 @@ def _map_bounds_to_constraints(self, test_bounds): """ Convert input bounds into a form acceptable by cvxpy and add to the constraints list. - :param test_bounds: minimum and maximum weight of each asset OR single min/max pair - if all identical OR pair of arrays corresponding to lower/upper bounds. defaults to (0, 1). - :type test_bounds: tuple OR list/tuple of tuples OR pair of np arrays - :raises TypeError: if ``test_bounds`` is not of the right type - :return: bounds suitable for cvxpy - :rtype: tuple pair of np.ndarray + Parameters + ---------- + test_bounds : tuple or list/tuple of tuples or pair of np arrays + minimum and maximum weight of each asset OR single min/max pair + if all identical OR pair of arrays corresponding to lower/upper bounds. defaults to (0, 1). + + Raises + ------ + TypeError + if ``test_bounds`` is not of the right type + + Returns + ------- + tuple pair of np.ndarray + bounds suitable for cvxpy """ # If it is a collection with the right length, assume they are all bounds. if len(test_bounds) == self.n_assets and not isinstance( @@ -284,7 +306,10 @@ def _solve_cvxpy_opt_problem(self): Helper method to solve the cvxpy problem and check output, once objectives and constraints have been defined - :raises exceptions.OptimizationError: if problem is not solvable by cvxpy + Raises + ------ + exceptions.OptimizationError + if problem is not solvable by cvxpy """ try: if self._opt is None: @@ -330,8 +355,10 @@ def L1_norm(w, k=1): ef.add_objective(L1_norm, k=2) - :param new_objective: the objective to be added - :type new_objective: cp.Expression (i.e function of cp.Variable) + Parameters + ---------- + new_objective : cp.Expression + the objective to be added (i.e function of cp.Variable) """ if self._opt is not None: raise exceptions.InstantiationError( @@ -351,8 +378,10 @@ def add_constraint(self, new_constraint): ef.add_constraint(lambda x : x >= 0.01) ef.add_constraint(lambda x: x <= np.array([0.01, 0.08, ..., 0.5])) - :param new_constraint: the constraint to be added - :type new_constraint: callable (e.g lambda function) + Parameters + ---------- + new_constraint : callable + the constraint to be added (e.g lambda function) """ if not callable(new_constraint): raise TypeError( @@ -386,12 +415,14 @@ def add_sector_constraints(self, sector_mapper, sector_lower, sector_upper): "Oil/Gas": 0.1 # less than 10% oil and gas } - :param sector_mapper: dict that maps tickers to sectors - :type sector_mapper: {str: str} dict - :param sector_lower: lower bounds for each sector - :type sector_lower: {str: float} dict - :param sector_upper: upper bounds for each sector - :type sector_upper: {str:float} dict + Parameters + ---------- + sector_mapper : {str: str} dict + dict that maps tickers to sectors + sector_lower : {str: float} dict + lower bounds for each sector + sector_upper : {str: float} dict + upper bounds for each sector """ if np.any(self._lower_bounds < 0): warnings.warn( @@ -416,14 +447,23 @@ def logarithmic_barrier(w, cov_matrix, k=0.1): w = ef.convex_objective(logarithmic_barrier, cov_matrix=ef.cov_matrix) - :param custom_objective: an objective function to be MINIMISED. This should be written using - cvxpy atoms Should map (w, `**kwargs`) -> float. - :type custom_objective: function with signature (cp.Variable, `**kwargs`) -> cp.Expression - :param weights_sum_to_one: whether to add the default objective, defaults to True - :type weights_sum_to_one: bool, optional - :raises OptimizationError: if the objective is nonconvex or constraints nonlinear. - :return: asset weights for the efficient risk portfolio - :rtype: OrderedDict + Parameters + ---------- + custom_objective : function with signature (cp.Variable, **kwargs) -> cp.Expression + an objective function to be MINIMISED. This should be written using + cvxpy atoms Should map (w, ``**kwargs``) -> float. + weights_sum_to_one : bool, optional + whether to add the default objective, defaults to True + + Raises + ------ + OptimizationError + if the objective is nonconvex or constraints nonlinear. + + Returns + ------- + OrderedDict + asset weights for the efficient risk portfolio """ # custom_objective must have the right signature (w, **kwargs) self._objective = custom_objective(self._w, **kwargs) @@ -465,22 +505,27 @@ def nonconvex_objective( constraints=constraints, ) - :param objective_function: an objective function to be MINIMISED. This function - should map (weight, args) -> cost - :type objective_function: function with signature (np.ndarray, args) -> float - :param objective_args: arguments for the objective function (excluding weight) - :type objective_args: tuple of np.ndarrays - :param weights_sum_to_one: whether to add the default objective, defaults to True - :type weights_sum_to_one: bool, optional - :param constraints: list of constraints in the scipy format (i.e dicts) - :type constraints: dict list - :param solver: which SCIPY solver to use, e.g "SLSQP", "COBYLA", "BFGS". - User beware: different optimizers require different inputs. - :type solver: string - :param initial_guess: the initial guess for the weights, shape (n,) or (n, 1) - :type initial_guess: np.ndarray - :return: asset weights that optimize the custom objective - :rtype: OrderedDict + Parameters + ---------- + objective_function : function with signature (np.ndarray, args) -> float + an objective function to be MINIMISED. This function + should map (weight, args) -> cost + objective_args : tuple of np.ndarrays + arguments for the objective function (excluding weight) + weights_sum_to_one : bool, optional + whether to add the default objective, defaults to True + constraints : dict list + list of constraints in the scipy format (i.e dicts) + solver : string + which SCIPY solver to use, e.g "SLSQP", "COBYLA", "BFGS". + User beware: different optimizers require different inputs. + initial_guess : np.ndarray + the initial guess for the weights, shape (n,) or (n, 1) + + Returns + ------- + OrderedDict + asset weights that optimize the custom objective """ # Sanitise inputs if not isinstance(objective_args, tuple): @@ -519,20 +564,29 @@ def portfolio_performance( After optimising, calculate (and optionally print) the performance of the optimal portfolio. Currently calculates expected return, volatility, and the Sharpe ratio. - :param expected_returns: expected returns for each asset. Can be None if - optimising for volatility only (but not recommended). - :type expected_returns: np.ndarray or pd.Series - :param cov_matrix: covariance of returns for each asset - :type cov_matrix: np.array or pd.DataFrame - :param weights: weights or assets - :type weights: list, np.array or dict, optional - :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.0 - :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) + Parameters + ---------- + expected_returns : np.ndarray or pd.Series + expected returns for each asset. Can be None if + optimising for volatility only (but not recommended). + cov_matrix : np.array or pd.DataFrame + covariance of returns for each asset + weights : list, np.array or dict, optional + weights or assets + verbose : bool, optional + whether performance should be printed, defaults to False + risk_free_rate : float, optional + risk-free rate of borrowing/lending, defaults to 0.0 + + Raises + ------ + ValueError + if weights have not been calculated yet + + Returns + ------- + (float, float, float) + expected return, volatility, Sharpe ratio. """ if isinstance(weights, dict): if isinstance(expected_returns, pd.Series): @@ -582,10 +636,15 @@ def _get_all_args(expression: cp.Expression) -> List[cp.Expression]: """ Helper function to recursively get all arguments from a cvxpy expression - :param expression: input cvxpy expression - :type expression: cp.Expression - :return: a list of cvxpy arguments - :rtype: List[cp.Expression] + Parameters + ---------- + expression : cp.Expression + input cvxpy expression + + Returns + ------- + List[cp.Expression] + a list of cvxpy arguments """ if expression.args == []: return [expression] diff --git a/pypfopt/black_litterman.py b/pypfopt/black_litterman.py index 61281d4a..37f0fb1d 100644 --- a/pypfopt/black_litterman.py +++ b/pypfopt/black_litterman.py @@ -28,18 +28,23 @@ def market_implied_prior_returns( \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.0. - 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 + Parameters + ---------- + market_caps : {ticker: cap} dict or pd.Series + market capitalisations of all assets + risk_aversion : positive float + risk aversion parameter + cov_matrix : pd.DataFrame + covariance matrix of asset returns + risk_free_rate : float, optional + risk-free rate of borrowing/lending, defaults to 0.0. + You should use the appropriate time period, corresponding + to the covariance matrix. + + Returns + ------- + pd.Series + prior estimate of returns as implied by the market caps """ if not isinstance(cov_matrix, pd.DataFrame): warnings.warn( @@ -63,16 +68,25 @@ def market_implied_risk_aversion(market_prices, frequency=252, risk_free_rate=0. \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: annualised risk-free rate of borrowing/lending, defaults to 0.0. - :type risk_free_rate: float, optional - :raises TypeError: if market_prices cannot be parsed - :return: market-implied risk aversion - :rtype: float + Parameters + ---------- + market_prices : pd.Series with DatetimeIndex. + the (daily) prices of the market portfolio, e.g SPY. + frequency : int, optional + number of time periods in a year, defaults to 252 (the number + of trading days in a year) + risk_free_rate : float, optional + annualised risk-free rate of borrowing/lending, defaults to 0.0. + + Raises + ------ + TypeError + if market_prices cannot be parsed + + Returns + ------- + float + market-implied risk aversion """ if not isinstance(market_prices, (pd.Series, pd.DataFrame)): raise TypeError("Please format market_prices as a pd.Series") @@ -138,36 +152,38 @@ def __init__( **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.0 + Parameters + ---------- + cov_matrix : pd.DataFrame or np.ndarray + NxN covariance matrix of returns + pi : np.ndarray or pd.Series, optional + 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. + absolute_views : pd.Series or dict, optional + a collection of K absolute views on a subset of assets, + defaults to None. If this is provided, we do not need P, Q. + Q : np.ndarray or pd.DataFrame, optional + Kx1 views vector, defaults to None + P : np.ndarray or pd.DataFrame, optional + KxN picking matrix, defaults to None + omega : np.ndarray or pd.DataFrame, or string, optional + 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. + view_confidences : np.ndarray, pd.Series, list, optional + Kx1 vector of percentage view confidences (between 0 and 1), + required to compute omega via Idzorek's method. + tau : float, optional + the weight-on-views scalar (default is 0.05) + risk_aversion : positive float, optional + risk aversion parameter, defaults to 1 + market_caps : np.ndarray or pd.Series, optional + (kwarg) market caps for the assets, required if pi="market" + risk_free_rate : float, optional + (kwarg) risk_free_rate is needed in some methods. Defaults to 0.0. """ if sys.version_info[1] == 5: # pragma: no cover warnings.warn( @@ -213,8 +229,10 @@ def _parse_views(self, absolute_views): {"AAPL": 0.20, "GOOG": 0.12, "XOM": -0.30} - :param absolute_views: absolute views on asset performances - :type absolute_views: dict, pd.Series + Parameters + ---------- + absolute_views : dict or pd.Series + absolute views on asset performances """ if not isinstance(absolute_views, (dict, pd.Series)): raise TypeError("views should be a dict or pd.Series") @@ -333,7 +351,10 @@ 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. + Raises + ------ + ValueError + if there are incorrect dimensions. """ N = self.n_assets K = len(self.Q) @@ -348,8 +369,10 @@ def default_omega(cov_matrix, P, tau): He and Litterman (1999), such that the ratio omega/tau is proportional to the variance of the view portfolio. - :return: KxK diagonal uncertainty matrix - :rtype: np.ndarray + Returns + ------- + np.ndarray + KxK diagonal uncertainty matrix """ return np.diag(np.diag(tau * P @ cov_matrix @ P.T)) @@ -360,11 +383,16 @@ def idzorek_method(view_confidences, cov_matrix, pi, Q, P, tau, risk_aversion=1) percentage confidences. We use the closed-form solution described by Jay Walters in 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 + Parameters + ---------- + view_confidences : np.ndarray, pd.Series, or list, optional + Kx1 vector of percentage view confidences (between 0 and 1), + required to compute omega via Idzorek's method. + + Returns + ------- + np.ndarray + KxK diagonal uncertainty matrix """ view_omegas = [] for view_idx in range(len(Q)): @@ -391,8 +419,10 @@ def bl_returns(self): Calculate the posterior estimate of the returns vector, given views on some assets. - :return: posterior returns vector - :rtype: pd.Series + Returns + ------- + pd.Series + posterior returns vector """ if self._tau_sigma_P is None: @@ -419,8 +449,10 @@ def bl_cov(self): It is assumed that omega is diagonal. If this is not the case, please manually set omega_inv. - :return: posterior covariance matrix - :rtype: pd.DataFrame + Returns + ------- + pd.DataFrame + posterior covariance matrix """ if self._tau_sigma_P is None: self._tau_sigma_P = self.tau * self.cov_matrix @ self.P.T @@ -450,10 +482,15 @@ def bl_weights(self, risk_aversion=None): 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 + Parameters + ---------- + risk_aversion : positive float, optional + risk aversion parameter, defaults to 1 + + Returns + ------- + OrderedDict + asset weights implied by returns """ if risk_aversion is None: risk_aversion = self.risk_aversion @@ -484,15 +521,24 @@ def portfolio_performance(self, verbose=False, risk_free_rate=0.0): portfolio. Currently calculates expected return, volatility, and the Sharpe ratio. This method uses the BL posterior returns and covariance matrix. - :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.0. - 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) + Parameters + ---------- + verbose : bool, optional + whether performance should be printed, defaults to False + risk_free_rate : float, optional + risk-free rate of borrowing/lending, defaults to 0.0. + The period of the risk-free rate should correspond to the + frequency of expected returns. + + Raises + ------ + ValueError + if weights have not been calculated yet + + Returns + ------- + (float, float, float) + expected return, volatility, Sharpe ratio. """ if self.posterior_cov is None: self.posterior_cov = self.bl_cov() diff --git a/pypfopt/cla.py b/pypfopt/cla.py index 3ffc77cf..bc6d59d3 100644 --- a/pypfopt/cla.py +++ b/pypfopt/cla.py @@ -49,16 +49,23 @@ class CLA(base_optimizer.BaseOptimizer): def __init__(self, expected_returns, cov_matrix, weight_bounds=(0, 1)): """ - :param expected_returns: expected returns for each asset. Set to None if - optimising for volatility only. - :type expected_returns: pd.Series, list, np.ndarray - :param cov_matrix: covariance of returns for each asset - :type cov_matrix: pd.DataFrame or np.array - :param weight_bounds: minimum and maximum weight of an asset, defaults to (0, 1). - Must be changed to (-1, 1) for portfolios with shorting. - :type weight_bounds: tuple (float, float) or (list/ndarray, list/ndarray) or list(tuple(float, float)) - :raises TypeError: if ``expected_returns`` is not a series, list or array - :raises TypeError: if ``cov_matrix`` is not a dataframe or array + Parameters + ---------- + expected_returns : pd.Series, list, or np.ndarray + expected returns for each asset. Set to None if + optimising for volatility only. + cov_matrix : pd.DataFrame or np.array + covariance of returns for each asset + weight_bounds : tuple (float, float) or (list/ndarray, list/ndarray) or list(tuple(float, float)) + minimum and maximum weight of an asset, defaults to (0, 1). + Must be changed to (-1, 1) for portfolios with shorting. + + Raises + ------ + TypeError + if ``expected_returns`` is not a series, list or array + TypeError + if ``cov_matrix`` is not a dataframe or array """ # Initialize the class self.mean = np.array(expected_returns).reshape((len(expected_returns), 1)) @@ -101,10 +108,15 @@ def _infnone(x): """ Helper method to map None to float infinity. - :param x: argument - :type x: float - :return: infinity if the argument was None otherwise x - :rtype: float + Parameters + ---------- + x : float + argument + + Returns + ------- + float + infinity if the argument was None otherwise x """ return float("-inf") if x is None else x @@ -205,14 +217,19 @@ def _reduce_matrix(matrix, listX, listY): which is significantly faster than the previous nested loop implementation for large matrices. - :param matrix: input matrix to extract submatrix from - :type matrix: np.ndarray - :param listX: row indices to select - :type listX: list - :param listY: column indices to select - :type listY: list - :return: submatrix with selected rows and columns, or None if indices are empty - :rtype: np.ndarray or None + Parameters + ---------- + matrix : np.ndarray + input matrix to extract submatrix from + listX : list + row indices to select + listY : list + column indices to select + + Returns + ------- + np.ndarray or None + submatrix with selected rows and columns, or None if indices are empty """ if len(listX) == 0 or len(listY) == 0: return None @@ -384,8 +401,10 @@ def max_sharpe(self): """ Maximise the Sharpe ratio. - :return: asset weights for the max-sharpe portfolio - :rtype: OrderedDict + Returns + ------- + OrderedDict + asset weights for the max-sharpe portfolio """ if not self.w: self._solve() @@ -406,8 +425,10 @@ def min_volatility(self): """ Minimise volatility. - :return: asset weights for the volatility-minimising portfolio - :rtype: OrderedDict + Returns + ------- + OrderedDict + asset weights for the volatility-minimising portfolio """ if not self.w: self._solve() @@ -423,11 +444,20 @@ def efficient_frontier(self, points=100): """ Efficiently compute the entire efficient frontier - :param points: rough number of points to evaluate, defaults to 100 - :type points: int, optional - :raises ValueError: if weights have not been computed - :return: return list, std list, weight list - :rtype: (float list, float list, np.ndarray list) + Parameters + ---------- + points : int, optional + rough number of points to evaluate, defaults to 100 + + Raises + ------ + ValueError + if weights have not been computed + + Returns + ------- + (float list, float list, np.ndarray list) + return list, std list, weight list """ if not self.w: self._solve() @@ -459,13 +489,22 @@ def portfolio_performance(self, verbose=False, risk_free_rate=0.0): After optimising, calculate (and optionally print) the performance of the optimal portfolio. Currently calculates expected return, volatility, and the Sharpe ratio. - :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.0 - :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) + Parameters + ---------- + verbose : bool, optional + whether performance should be printed, defaults to False + risk_free_rate : float, optional + risk-free rate of borrowing/lending, defaults to 0.0 + + Raises + ------ + ValueError + if weights have not been calculated yet + + Returns + ------- + (float, float, float) + expected return, volatility, Sharpe ratio. """ return base_optimizer.portfolio_performance( self.weights, diff --git a/pypfopt/discrete_allocation.py b/pypfopt/discrete_allocation.py index 4e4d8d4c..f4033e0d 100644 --- a/pypfopt/discrete_allocation.py +++ b/pypfopt/discrete_allocation.py @@ -19,11 +19,20 @@ def get_latest_prices(prices): A helper tool which retrieves the most recent asset prices from a dataframe of asset prices, required in order to generate a discrete allocation. - :param prices: historical asset prices - :type prices: pd.DataFrame - :raises TypeError: if prices are not in a dataframe - :return: the most recent price of each asset - :rtype: pd.Series + Parameters + ---------- + prices : pd.DataFrame + historical asset prices + + Raises + ------ + TypeError + if prices are not in a dataframe + + Returns + ------- + pd.Series + the most recent price of each asset """ if not isinstance(prices, pd.DataFrame): raise TypeError("prices not in a dataframe") @@ -55,18 +64,26 @@ def __init__( self, weights, latest_prices, total_portfolio_value=10000, short_ratio=None ): """ - :param weights: continuous weights generated from the ``efficient_frontier`` module - :type weights: dict - :param latest_prices: the most recent price for each asset - :type latest_prices: pd.Series - :param total_portfolio_value: the desired total value of the portfolio, defaults to 10000 - :type total_portfolio_value: int/float, optional - :param short_ratio: the short ratio, e.g 0.3 corresponds to 130/30. If None, - defaults to the input weights. - :type short_ratio: float, defaults to None. - :raises TypeError: if ``weights`` is not a dict - :raises TypeError: if ``latest_prices`` isn't a series - :raises ValueError: if ``short_ratio < 0`` + Parameters + ---------- + weights : dict + continuous weights generated from the ``efficient_frontier`` module + latest_prices : pd.Series + the most recent price for each asset + total_portfolio_value : int or float, optional + the desired total value of the portfolio, defaults to 10000 + short_ratio : float, optional + the short ratio, e.g 0.3 corresponds to 130/30. If None, + defaults to the input weights. Defaults to None. + + Raises + ------ + TypeError + if ``weights`` is not a dict + TypeError + if ``latest_prices`` isn't a series + ValueError + if ``short_ratio < 0`` """ if not isinstance(weights, dict): raise TypeError("weights should be a dictionary of {ticker: weight}") @@ -93,7 +110,9 @@ def _remove_zero_positions(allocation): """ Utility function to remove zero positions (i.e with no shares being bought) - :type allocation: dict + Parameters + ---------- + allocation : dict """ return {k: v for k, v in allocation.items() if v != 0} @@ -103,10 +122,15 @@ def _allocation_rmse_error(self, verbose=True): weights and continuous weights. RMSE was used instead of MAE because we want to penalise large variations. - :param verbose: print weight discrepancies? - :type verbose: bool - :return: rmse error - :rtype: float + Parameters + ---------- + verbose : bool + print weight discrepancies? + + Returns + ------- + float + rmse error """ portfolio_val = 0 for ticker, num in self.allocation.items(): @@ -136,13 +160,18 @@ def greedy_portfolio(self, reinvest=False, verbose=False): Convert continuous weights into a discrete portfolio allocation using a greedy iterative approach. - :param reinvest: whether or not to reinvest cash gained from shorting - :type reinvest: bool, defaults to False - :param verbose: print error analysis? - :type verbose: bool, defaults to False - :return: the number of shares of each ticker that should be purchased, - along with the amount of funds leftover. - :rtype: (dict, float) + Parameters + ---------- + reinvest : bool, optional + whether or not to reinvest cash gained from shorting. Defaults to False. + verbose : bool, optional + print error analysis? Defaults to False. + + Returns + ------- + (dict, float) + the number of shares of each ticker that should be purchased, + along with the amount of funds leftover. """ # Sort in descending order of weight self.weights.sort(key=lambda x: x[1], reverse=True) @@ -260,15 +289,21 @@ def lp_portfolio(self, reinvest=False, verbose=False, solver=None): Convert continuous weights into a discrete portfolio allocation using integer programming. - :param reinvest: whether or not to reinvest cash gained from shorting - :type reinvest: bool, defaults to False - :param verbose: print error analysis? - :type verbose: bool - :param solver: the CVXPY solver to use (must support mixed-integer programs) - :type solver: str, defaults to "ECOS_BB" if ecos is installed, else None - :return: the number of shares of each ticker that should be purchased, along with the amount - of funds leftover. - :rtype: (dict, float) + Parameters + ---------- + reinvest : bool, optional + whether or not to reinvest cash gained from shorting. Defaults to False. + verbose : bool, optional + print error analysis? Defaults to False. + solver : str, optional + the CVXPY solver to use (must support mixed-integer programs). + Defaults to "ECOS_BB" if ecos is installed, else None. + + Returns + ------- + (dict, float) + the number of shares of each ticker that should be purchased, along with the amount + of funds leftover. """ # todo 1.7.0: remove this defaulting behavior if solver is None and _check_soft_dependencies("ecos", severity="none"): diff --git a/pypfopt/efficient_frontier/efficient_cdar.py b/pypfopt/efficient_frontier/efficient_cdar.py index febd6e35..8983ef45 100644 --- a/pypfopt/efficient_frontier/efficient_cdar.py +++ b/pypfopt/efficient_frontier/efficient_cdar.py @@ -56,24 +56,31 @@ def __init__( solver_options=None, ): """ - :param expected_returns: expected returns for each asset. Can be None if - optimising for CDaR 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 beta: confidence level, defaults to 0.95 (i.e expected drawdown on the worst (1-beta) days). - :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 + Parameters + ---------- + expected_returns : pd.Series, list, or np.ndarray + expected returns for each asset. Can be None if + optimising for CDaR only. + returns : pd.DataFrame or np.array + (historic) returns for all your assets (no NaNs). + See ``expected_returns.returns_from_prices``. + beta : float + confidence level, defaults to 0.95 (i.e expected drawdown on the worst (1-beta) days). + weight_bounds : tuple or list of tuples, optional + 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. + solver : str + name of solver. list available solvers with: `cvxpy.installed_solvers()` + verbose : bool, optional + whether performance and debugging info should be printed, defaults to False + solver_options : dict, optional + parameters for the given solver + + Raises + ------ + TypeError + if ``expected_returns`` is not a series, list or array """ super().__init__( expected_returns=expected_returns, @@ -117,11 +124,16 @@ def min_cdar(self, market_neutral=False): """ Minimise portfolio CDaR (see docs for further explanation). - :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 + Parameters + ---------- + market_neutral : bool, optional + whether the portfolio should be market neutral (weights sum to zero), + defaults to False. Requires negative lower weight bound. + + Returns + ------- + OrderedDict + asset weights for the volatility-minimising portfolio """ self._objective = self._alpha + 1.0 / ( len(self.returns) * (1 - self._beta) @@ -138,15 +150,25 @@ def efficient_return(self, target_return, market_neutral=False): """ Minimise CDaR for a given target return. - :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 + Parameters + ---------- + target_return : float + the desired return of the resulting portfolio. + market_neutral : bool, optional + whether the portfolio should be market neutral (weights sum to zero), + defaults to False. Requires negative lower weight bound. + + Raises + ------ + ValueError + if ``target_return`` is not a positive float + ValueError + if no portfolio can be found with return equal to ``target_return`` + + Returns + ------- + OrderedDict + asset weights for the optimal portfolio """ update_existing_parameter = self.is_parameter_defined("target_return") @@ -168,13 +190,18 @@ def efficient_risk(self, target_cdar, market_neutral=False): The resulting portfolio will have a CDaR less than the target (but not guaranteed to be equal). - :param target_cdar: the desired maximum CDaR of the resulting portfolio. - :type target_cdar: 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 + Parameters + ---------- + target_cdar : float + the desired maximum CDaR of the resulting portfolio. + market_neutral : bool, optional + whether the portfolio should be market neutral (weights sum to zero), + defaults to False. Requires negative lower weight bound. + + Returns + ------- + OrderedDict + asset weights for the efficient risk portfolio """ update_existing_parameter = self.is_parameter_defined("target_cdar") @@ -215,11 +242,20 @@ def portfolio_performance(self, verbose=False): After optimising, calculate (and optionally print) the performance of the optimal portfolio, specifically: expected return, CDaR - :param verbose: whether performance should be printed, defaults to False - :type verbose: bool, optional - :raises ValueError: if weights have not been calculated yet - :return: expected return, CDaR. - :rtype: (float, float) + Parameters + ---------- + verbose : bool, optional + whether performance should be printed, defaults to False + + Raises + ------ + ValueError + if weights have not been calculated yet + + Returns + ------- + (float, float) + expected return, CDaR. """ mu = objective_functions.portfolio_return( self.weights, self.expected_returns, negative=False diff --git a/pypfopt/efficient_frontier/efficient_cvar.py b/pypfopt/efficient_frontier/efficient_cvar.py index d456f799..2cb21bb6 100644 --- a/pypfopt/efficient_frontier/efficient_cvar.py +++ b/pypfopt/efficient_frontier/efficient_cvar.py @@ -57,24 +57,31 @@ def __init__( solver_options=None, ): """ - :param expected_returns: expected returns for each asset. Can be None if - optimising for conditional value at risk 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 beta: confidence level, defauls to 0.95 (i.e expected loss on the worst (1-beta) days). - :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 + Parameters + ---------- + expected_returns : pd.Series, list, or np.ndarray + expected returns for each asset. Can be None if + optimising for conditional value at risk only. + returns : pd.DataFrame or np.array + (historic) returns for all your assets (no NaNs). + See ``expected_returns.returns_from_prices``. + beta : float + confidence level, defauls to 0.95 (i.e expected loss on the worst (1-beta) days). + weight_bounds : tuple or list of tuples, optional + 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. + solver : str + name of solver. list available solvers with: `cvxpy.installed_solvers()` + verbose : bool, optional + whether performance and debugging info should be printed, defaults to False + solver_options : dict, optional + parameters for the given solver + + Raises + ------ + TypeError + if ``expected_returns`` is not a series, list or array """ super().__init__( expected_returns=expected_returns, @@ -117,11 +124,16 @@ def min_cvar(self, market_neutral=False): """ Minimise portfolio CVaR (see docs for further explanation). - :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 + Parameters + ---------- + market_neutral : bool, optional + whether the portfolio should be market neutral (weights sum to zero), + defaults to False. Requires negative lower weight bound. + + Returns + ------- + OrderedDict + asset weights for the volatility-minimising portfolio """ self._objective = self._alpha + 1.0 / ( len(self.returns) * (1 - self._beta) @@ -142,15 +154,25 @@ def efficient_return(self, target_return, market_neutral=False): """ Minimise CVaR for a given target return. - :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 + Parameters + ---------- + target_return : float + the desired return of the resulting portfolio. + market_neutral : bool, optional + whether the portfolio should be market neutral (weights sum to zero), + defaults to False. Requires negative lower weight bound. + + Raises + ------ + ValueError + if ``target_return`` is not a positive float + ValueError + if no portfolio can be found with return equal to ``target_return`` + + Returns + ------- + OrderedDict + asset weights for the optimal portfolio """ update_existing_parameter = self.is_parameter_defined("target_return") if update_existing_parameter: @@ -182,13 +204,18 @@ def efficient_risk(self, target_cvar, market_neutral=False): The resulting portfolio will have a CVaR less than the target (but not guaranteed to be equal). - :param target_cvar: the desired conditional value at risk of the resulting portfolio. - :type target_cvar: 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 + Parameters + ---------- + target_cvar : float + the desired conditional value at risk of the resulting portfolio. + market_neutral : bool, optional + whether the portfolio should be market neutral (weights sum to zero), + defaults to False. Requires negative lower weight bound. + + Returns + ------- + OrderedDict + asset weights for the efficient risk portfolio """ update_existing_parameter = self.is_parameter_defined("target_cvar") if update_existing_parameter: @@ -222,11 +249,20 @@ def portfolio_performance(self, verbose=False): After optimising, calculate (and optionally print) the performance of the optimal portfolio, specifically: expected return, CVaR - :param verbose: whether performance should be printed, defaults to False - :type verbose: bool, optional - :raises ValueError: if weights have not been calculated yet - :return: expected return, CVaR. - :rtype: (float, float) + Parameters + ---------- + verbose : bool, optional + whether performance should be printed, defaults to False + + Raises + ------ + ValueError + if weights have not been calculated yet + + Returns + ------- + (float, float) + expected return, CVaR. """ mu = objective_functions.portfolio_return( self.weights, self.expected_returns, negative=False diff --git a/pypfopt/efficient_frontier/efficient_frontier.py b/pypfopt/efficient_frontier/efficient_frontier.py index b428dcc5..33d195de 100644 --- a/pypfopt/efficient_frontier/efficient_frontier.py +++ b/pypfopt/efficient_frontier/efficient_frontier.py @@ -62,24 +62,31 @@ def __init__( solver_options=None, ): """ - :param expected_returns: expected returns for each asset. Can be None if - optimising for volatility only (but not recommended). - :type expected_returns: pd.Series, list, np.ndarray - :param cov_matrix: covariance of returns for each asset. This **must** be - positive semidefinite, otherwise optimization will fail. - :type cov_matrix: pd.DataFrame or np.array - :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 - :raises TypeError: if ``cov_matrix`` is not a dataframe or array + Parameters + ---------- + expected_returns : pd.Series, list, or np.ndarray + expected returns for each asset. Can be None if + optimising for volatility only (but not recommended). + cov_matrix : pd.DataFrame or np.array + covariance of returns for each asset. This **must** be + positive semidefinite, otherwise optimization will fail. + weight_bounds : tuple or list of tuples, optional + 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. + solver : str + name of solver. list available solvers with: `cvxpy.installed_solvers()` + verbose : bool, optional + whether performance and debugging info should be printed, defaults to False + solver_options : dict, optional + parameters for the given solver + + Raises + ------ + TypeError + if ``expected_returns`` is not a series, list or array + TypeError + if ``cov_matrix`` is not a dataframe or array """ # Inputs self.cov_matrix = self._validate_cov_matrix(cov_matrix) @@ -187,8 +194,10 @@ def min_volatility(self): """ Minimise volatility. - :return: asset weights for the volatility-minimising portfolio - :rtype: OrderedDict + Returns + ------- + OrderedDict + asset weights for the volatility-minimising portfolio """ self._objective = objective_functions.portfolio_variance( self._w, self.cov_matrix @@ -203,8 +212,10 @@ def _max_return(self, return_value=True): """ Helper method to maximise return. This should not be used to optimize a portfolio. - :return: asset weights for the return-minimising portfolio - :rtype: OrderedDict + Returns + ------- + OrderedDict + asset weights for the return-minimising portfolio """ if self.expected_returns is None: raise ValueError("no expected returns provided") @@ -230,13 +241,22 @@ def max_sharpe(self, risk_free_rate=0.0): This is a convex optimization problem after making a certain variable substitution. See `Cornuejols and Tutuncu (2006) `_ for more. - :param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.0. - The period of the risk-free rate should correspond to the - frequency of expected returns. - :type risk_free_rate: float, optional - :raises ValueError: if ``risk_free_rate`` is non-numeric - :return: asset weights for the Sharpe-maximising portfolio - :rtype: OrderedDict + Parameters + ---------- + risk_free_rate : float, optional + risk-free rate of borrowing/lending, defaults to 0.0. + The period of the risk-free rate should correspond to the + frequency of expected returns. + + Raises + ------ + ValueError + if ``risk_free_rate`` is non-numeric + + Returns + ------- + OrderedDict + asset weights for the Sharpe-maximising portfolio """ if not isinstance(risk_free_rate, (int, float)): raise ValueError("risk_free_rate should be numeric") @@ -300,14 +320,19 @@ def max_quadratic_utility(self, risk_aversion=1, market_neutral=False): \max_w w^T \mu - \frac \delta 2 w^T \Sigma w - :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 + Parameters + ---------- + risk_aversion : positive float + risk aversion parameter (must be greater than 0), + defaults to 1 + market_neutral : bool, optional + whether the portfolio should be market neutral (weights sum to zero), + defaults to False. Requires negative lower weight bound. + + Returns + ------- + OrderedDict + asset weights for the maximum-utility portfolio """ if risk_aversion <= 0: raise ValueError("risk aversion coefficient must be greater than zero") @@ -334,16 +359,27 @@ def efficient_risk(self, target_volatility, market_neutral=False): Maximise return for a target risk. The resulting portfolio will have a volatility less than the target (but not guaranteed to be equal). - :param target_volatility: the desired maximum volatility of the resulting portfolio. - :type target_volatility: 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 - :raises ValueError: if ``target_volatility`` is not a positive float - :raises ValueError: if no portfolio can be found with volatility equal to ``target_volatility`` - :raises ValueError: if ``risk_free_rate`` is non-numeric - :return: asset weights for the efficient risk portfolio - :rtype: OrderedDict + Parameters + ---------- + target_volatility : float + the desired maximum volatility of the resulting portfolio. + market_neutral : bool, optional + whether the portfolio should be market neutral (weights sum to zero), + defaults to False. Requires negative lower weight bound. + + Raises + ------ + ValueError + if ``target_volatility`` is not a positive float + ValueError + if no portfolio can be found with volatility equal to ``target_volatility`` + ValueError + if ``risk_free_rate`` is non-numeric + + Returns + ------- + OrderedDict + asset weights for the efficient risk portfolio """ if not isinstance(target_volatility, (float, int)) or target_volatility < 0: raise ValueError("target_volatility should be a positive float") @@ -381,15 +417,25 @@ def efficient_return(self, target_return, market_neutral=False): """ Calculate the 'Markowitz portfolio', minimising volatility for a given target return. - :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 Markowitz portfolio - :rtype: OrderedDict + Parameters + ---------- + target_return : float + the desired return of the resulting portfolio. + market_neutral : bool, optional + whether the portfolio should be market neutral (weights sum to zero), + defaults to False. Requires negative lower weight bound. + + Raises + ------ + ValueError + if ``target_return`` is not a positive float + ValueError + if no portfolio can be found with return equal to ``target_return`` + + Returns + ------- + OrderedDict + asset weights for the Markowitz portfolio """ if not isinstance(target_return, float): raise ValueError("target_return should be a float") @@ -426,15 +472,24 @@ def portfolio_performance(self, verbose=False, risk_free_rate=0.0): After optimising, calculate (and optionally print) the performance of the optimal portfolio. Currently calculates expected return, volatility, and the Sharpe ratio. - :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.0. - 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) + Parameters + ---------- + verbose : bool, optional + whether performance should be printed, defaults to False + risk_free_rate : float, optional + risk-free rate of borrowing/lending, defaults to 0.0. + The period of the risk-free rate should correspond to the + frequency of expected returns. + + Raises + ------ + ValueError + if weights have not been calculated yet + + Returns + ------- + (float, float, float) + expected return, volatility, Sharpe ratio. """ if self._risk_free_rate is not None: if risk_free_rate != self._risk_free_rate: diff --git a/pypfopt/efficient_frontier/efficient_semivariance.py b/pypfopt/efficient_frontier/efficient_semivariance.py index 89d520c7..9dfd769e 100644 --- a/pypfopt/efficient_frontier/efficient_semivariance.py +++ b/pypfopt/efficient_frontier/efficient_semivariance.py @@ -59,31 +59,38 @@ def __init__( 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 + Parameters + ---------- + expected_returns : pd.Series, list, or np.ndarray + expected returns for each asset. Can be None if + optimising for semideviation only. + returns : pd.DataFrame or np.array + (historic) returns for all your assets (no NaNs). + See ``expected_returns.returns_from_prices``. + frequency : int, optional + 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``. + benchmark : float + 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. + weight_bounds : tuple or list of tuples, optional + 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. + solver : str + name of solver. list available solvers with: `cvxpy.installed_solvers()` + verbose : bool, optional + whether performance and debugging info should be printed, defaults to False + solver_options : dict, optional + parameters for the given solver + + Raises + ------ + TypeError + if ``expected_returns`` is not a series, list or array """ # Instantiate parent super().__init__( @@ -110,11 +117,16 @@ def min_semivariance(self, market_neutral=False): """ Minimise portfolio semivariance (see docs for further explanation). - :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 + Parameters + ---------- + market_neutral : bool, optional + whether the portfolio should be market neutral (weights sum to zero), + defaults to False. Requires negative lower weight bound. + + Returns + ------- + OrderedDict + asset weights for the volatility-minimising portfolio """ p = cp.Variable(self._T, nonneg=True) n = cp.Variable(self._T, nonneg=True) @@ -133,14 +145,19 @@ def max_quadratic_utility(self, risk_aversion=1, market_neutral=False): Maximise the given quadratic utility, using portfolio semivariance instead of variance. - :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 + Parameters + ---------- + risk_aversion : positive float + risk aversion parameter (must be greater than 0), + defaults to 1 + market_neutral : bool, optional + whether the portfolio should be market neutral (weights sum to zero), + defaults to False. Requires negative lower weight bound. + + Returns + ------- + OrderedDict + asset weights for the maximum-utility portfolio """ if risk_aversion <= 0: raise ValueError("risk aversion coefficient must be greater than zero") @@ -172,13 +189,18 @@ def efficient_risk(self, target_semideviation, market_neutral=False): The resulting portfolio will have a semideviation less than the target (but not guaranteed to be equal). - :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 + Parameters + ---------- + target_semideviation : float + the desired maximum semideviation of the resulting portfolio. + market_neutral : bool, optional + whether the portfolio should be market neutral (weights sum to zero), + defaults to False. Requires negative lower weight bound. + + Returns + ------- + OrderedDict + asset weights for the efficient risk portfolio """ update_existing_parameter = self.is_parameter_defined("target_semivariance") if update_existing_parameter: @@ -209,15 +231,25 @@ def efficient_return(self, target_return, market_neutral=False): """ Minimise semideviation for a given target return. - :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 + Parameters + ---------- + target_return : float + the desired return of the resulting portfolio. + market_neutral : bool, optional + whether the portfolio should be market neutral (weights sum to zero), + defaults to False. Requires negative lower weight bound. + + Raises + ------ + ValueError + if ``target_return`` is not a positive float + ValueError + if no portfolio can be found with return equal to ``target_return`` + + Returns + ------- + OrderedDict + asset weights for the optimal portfolio """ if not isinstance(target_return, float) or target_return < 0: raise ValueError("target_return should be a positive float") @@ -251,15 +283,24 @@ def portfolio_performance(self, verbose=False, risk_free_rate=0.0): After optimising, calculate (and optionally print) the performance of the optimal portfolio, specifically: expected return, semideviation, Sortino ratio. - :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.0. - 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, semideviation, Sortino ratio. - :rtype: (float, float, float) + Parameters + ---------- + verbose : bool, optional + whether performance should be printed, defaults to False + risk_free_rate : float, optional + risk-free rate of borrowing/lending, defaults to 0.0. + The period of the risk-free rate should correspond to the + frequency of expected returns. + + Raises + ------ + ValueError + if weights have not been calculated yet + + Returns + ------- + (float, float, float) + expected return, semideviation, Sortino ratio. """ mu = objective_functions.portfolio_return( self.weights, self.expected_returns, negative=False diff --git a/pypfopt/expected_returns.py b/pypfopt/expected_returns.py index b9ab02e2..bbbf69c6 100644 --- a/pypfopt/expected_returns.py +++ b/pypfopt/expected_returns.py @@ -42,13 +42,18 @@ def returns_from_prices(prices, log_returns=False): """ Calculate the returns given prices. - :param prices: adjusted (daily) closing prices of the asset, each row is a - date and each column is a ticker/id. - :type prices: pd.DataFrame - :param log_returns: whether to compute using log returns - :type log_returns: bool, defaults to False - :return: (daily) returns - :rtype: pd.DataFrame + Parameters + ---------- + prices : pd.DataFrame + adjusted (daily) closing prices of the asset, each row is a + date and each column is a ticker/id. + log_returns : bool, optional + whether to compute using log returns. Defaults to False. + + Returns + ------- + pd.DataFrame + (daily) returns """ if log_returns: returns = np.log(1 + prices.pct_change(fill_method=None)).dropna(how="all") @@ -63,12 +68,17 @@ def prices_from_returns(returns, log_returns=False): the initial prices are all set to 1, but it behaves as intended when passed to any PyPortfolioOpt method. - :param returns: (daily) percentage returns of the assets - :type returns: pd.DataFrame - :param log_returns: whether to compute using log returns - :type log_returns: bool, defaults to False - :return: (daily) pseudo-prices. - :rtype: pd.DataFrame + Parameters + ---------- + returns : pd.DataFrame + (daily) percentage returns of the assets + log_returns : bool, optional + whether to compute using log returns. Defaults to False. + + Returns + ------- + pd.DataFrame + (daily) pseudo-prices. """ if log_returns: ret = np.exp(returns) @@ -82,21 +92,29 @@ def return_model(prices, method="mean_historical_return", **kwargs): """ Compute an estimate of future returns, using the return model specified in ``method``. - :param prices: adjusted closing prices of the asset, each row is a date - and each column is a ticker/id. - :type prices: pd.DataFrame - :param returns_data: if true, the first argument is returns instead of prices. - :type returns_data: bool, defaults to False. - :param method: the return model to use. Should be one of: + Parameters + ---------- + prices : pd.DataFrame + adjusted closing prices of the asset, each row is a date + and each column is a ticker/id. + returns_data : bool, optional + if true, the first argument is returns instead of prices. Defaults to False. + method : str, optional + the return model to use. Should be one of: - ``mean_historical_return`` - ``ema_historical_return`` - ``capm_return`` - :type method: str, optional - :raises NotImplementedError: if the supplied method is not recognised - :return: annualised sample covariance matrix - :rtype: pd.DataFrame + Raises + ------ + NotImplementedError + if the supplied method is not recognised + + Returns + ------- + pd.DataFrame + annualised sample covariance matrix """ if method == "mean_historical_return": return mean_historical_return(prices, **kwargs) @@ -116,22 +134,27 @@ def mean_historical_return( Use ``compounding`` to toggle between the default geometric mean (CAGR) and the arithmetic mean. - :param prices: adjusted closing prices of the asset, each row is a date - and each column is a ticker/id. - :type prices: pd.DataFrame - :param returns_data: if true, the first argument is returns instead of prices. - These **should not** be log returns. - :type returns_data: bool, defaults to False. - :param compounding: computes geometric mean returns if True, - arithmetic otherwise, optional. - :type compounding: bool, defaults to True - :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 log_returns: whether to compute using log returns - :type log_returns: bool, defaults to False - :return: annualised mean (daily) return for each asset - :rtype: pd.Series + Parameters + ---------- + prices : pd.DataFrame + adjusted closing prices of the asset, each row is a date + and each column is a ticker/id. + returns_data : bool, optional + if true, the first argument is returns instead of prices. + These **should not** be log returns. Defaults to False. + compounding : bool, optional + computes geometric mean returns if True, + arithmetic otherwise. Defaults to True. + frequency : int, optional + number of time periods in a year, defaults to 252 (the number + of trading days in a year) + log_returns : bool, optional + whether to compute using log returns. Defaults to False. + + Returns + ------- + pd.Series + annualised mean (daily) return for each asset """ if not isinstance(prices, pd.DataFrame): warnings.warn("prices are not in a dataframe", RuntimeWarning) @@ -160,24 +183,29 @@ def ema_historical_return( Calculate the exponentially-weighted mean of (daily) historical returns, giving higher weight to more recent data. - :param prices: adjusted closing prices of the asset, each row is a date - and each column is a ticker/id. - :type prices: pd.DataFrame - :param returns_data: if true, the first argument is returns instead of prices. - These **should not** be log returns. - :type returns_data: bool, defaults to False. - :param compounding: computes geometric mean returns if True, - arithmetic otherwise, optional. - :type compounding: bool, defaults to True - :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 span: the time-span for the EMA, defaults to 500-day EMA. - :type span: int, optional - :param log_returns: whether to compute using log returns - :type log_returns: bool, defaults to False - :return: annualised exponentially-weighted mean (daily) return of each asset - :rtype: pd.Series + Parameters + ---------- + prices : pd.DataFrame + adjusted closing prices of the asset, each row is a date + and each column is a ticker/id. + returns_data : bool, optional + if true, the first argument is returns instead of prices. + These **should not** be log returns. Defaults to False. + compounding : bool, optional + computes geometric mean returns if True, + arithmetic otherwise. Defaults to True. + frequency : int, optional + number of time periods in a year, defaults to 252 (the number + of trading days in a year) + span : int, optional + the time-span for the EMA, defaults to 500-day EMA. + log_returns : bool, optional + whether to compute using log returns. Defaults to False. + + Returns + ------- + pd.Series + annualised exponentially-weighted mean (daily) return of each asset """ if not isinstance(prices, pd.DataFrame): warnings.warn("prices are not in a dataframe", RuntimeWarning) @@ -214,27 +242,32 @@ def capm_return( R_i = R_f + \\beta_i (E(R_m) - R_f) - :param prices: adjusted closing prices of the asset, each row is a date - and each column is a ticker/id. - :type prices: pd.DataFrame - :param market_prices: adjusted closing prices of the benchmark, defaults to None - :type market_prices: pd.DataFrame, optional - :param returns_data: if true, the first arguments are returns instead of prices. - :type returns_data: bool, defaults to False. - :param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.0. - You should use the appropriate time period, corresponding - to the frequency parameter. - :type risk_free_rate: float, optional - :param compounding: computes geometric mean returns if True, - arithmetic otherwise, optional. - :type compounding: bool, defaults to True - :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 log_returns: whether to compute using log returns - :type log_returns: bool, defaults to False - :return: annualised return estimate - :rtype: pd.Series + Parameters + ---------- + prices : pd.DataFrame + adjusted closing prices of the asset, each row is a date + and each column is a ticker/id. + market_prices : pd.DataFrame, optional + adjusted closing prices of the benchmark, defaults to None + returns_data : bool, optional + if true, the first arguments are returns instead of prices. Defaults to False. + risk_free_rate : float, optional + risk-free rate of borrowing/lending, defaults to 0.0. + You should use the appropriate time period, corresponding + to the frequency parameter. + compounding : bool, optional + computes geometric mean returns if True, + arithmetic otherwise. Defaults to True. + frequency : int, optional + number of time periods in a year, defaults to 252 (the number + of trading days in a year) + log_returns : bool, optional + whether to compute using log returns. Defaults to False. + + Returns + ------- + pd.Series + annualised return estimate """ if not isinstance(prices, pd.DataFrame): warnings.warn("prices are not in a dataframe", RuntimeWarning) diff --git a/pypfopt/hierarchical_portfolio.py b/pypfopt/hierarchical_portfolio.py index 52bfa177..1207cc12 100644 --- a/pypfopt/hierarchical_portfolio.py +++ b/pypfopt/hierarchical_portfolio.py @@ -52,11 +52,17 @@ class HRPOpt(base_optimizer.BaseOptimizer): def __init__(self, returns=None, cov_matrix=None): """ - :param returns: asset historical returns - :type returns: pd.DataFrame - :param cov_matrix: covariance of asset returns - :type cov_matrix: pd.DataFrame. - :raises TypeError: if ``returns`` is not a dataframe + Parameters + ---------- + returns : pd.DataFrame + asset historical returns + cov_matrix : pd.DataFrame + covariance of asset returns + + Raises + ------ + TypeError + if ``returns`` is not a dataframe """ if returns is None and cov_matrix is None: raise ValueError("Either returns or cov_matrix must be provided") @@ -79,12 +85,17 @@ def _get_cluster_var(cov, cluster_items): """ Compute the variance per cluster - :param cov: covariance matrix - :type cov: np.ndarray - :param cluster_items: tickers in the cluster - :type cluster_items: list - :return: the variance per cluster - :rtype: float + Parameters + ---------- + cov : np.ndarray + covariance matrix + cluster_items : list + tickers in the cluster + + Returns + ------- + float + the variance per cluster """ # Compute variance per cluster cov_slice = cov.loc[cluster_items, cluster_items] @@ -97,10 +108,15 @@ def _get_quasi_diag(link): """ Sort clustered items by distance - :param link: linkage matrix after clustering - :type link: np.ndarray - :return: sorted list of indices - :rtype: list + Parameters + ---------- + link : np.ndarray + linkage matrix after clustering + + Returns + ------- + list + sorted list of indices """ return sch.to_tree(link, rd=False).pre_order() @@ -110,12 +126,17 @@ def _raw_hrp_allocation(cov, ordered_tickers): Given the clusters, compute the portfolio that minimises risk by recursively traversing the hierarchical tree from the top. - :param cov: covariance matrix - :type cov: np.ndarray - :param ordered_tickers: list of tickers ordered by distance - :type ordered_tickers: str list - :return: raw portfolio weights - :rtype: pd.Series + Parameters + ---------- + cov : np.ndarray + covariance matrix + ordered_tickers : str list + list of tickers ordered by distance + + Returns + ------- + pd.Series + raw portfolio weights """ w = pd.Series(1.0, index=ordered_tickers) cluster_items = [ordered_tickers] # initialize all items in one cluster @@ -144,10 +165,15 @@ def optimize(self, linkage_method="single"): Construct a hierarchical risk parity portfolio, using Scipy hierarchical clustering (see `here `_) - :param linkage_method: which scipy linkage method to use - :type linkage_method: str - :return: weights for the HRP portfolio - :rtype: OrderedDict + Parameters + ---------- + linkage_method : str + which scipy linkage method to use + + Returns + ------- + OrderedDict + weights for the HRP portfolio """ if linkage_method not in sch._LINKAGE_METHODS: raise ValueError("linkage_method must be one recognised by scipy") @@ -179,18 +205,27 @@ def portfolio_performance(self, verbose=False, risk_free_rate=0.0, frequency=252 portfolio. Currently calculates expected return, volatility, and the Sharpe ratio assuming returns are daily - :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.0. - The period of the risk-free rate should correspond to the - frequency of expected returns. - :type risk_free_rate: float, optional - :param frequency: number of time periods in a year, defaults to 252 (the number - of trading days in a year) - :type frequency: int, optional - :raises ValueError: if weights have not been calculated yet - :return: expected return, volatility, Sharpe ratio. - :rtype: (float, float, float) + Parameters + ---------- + verbose : bool, optional + whether performance should be printed, defaults to False + risk_free_rate : float, optional + risk-free rate of borrowing/lending, defaults to 0.0. + The period of the risk-free rate should correspond to the + frequency of expected returns. + frequency : int, optional + number of time periods in a year, defaults to 252 (the number + of trading days in a year) + + Raises + ------ + ValueError + if weights have not been calculated yet + + Returns + ------- + (float, float, float) + expected return, volatility, Sharpe ratio. """ if self.returns is None: cov = self.cov_matrix diff --git a/pypfopt/objective_functions.py b/pypfopt/objective_functions.py index 38ea9ece..b7d14ce1 100644 --- a/pypfopt/objective_functions.py +++ b/pypfopt/objective_functions.py @@ -38,12 +38,17 @@ def _objective_value(w, obj): or the objective function as a cvxpy object depending on whether w is a cvxpy variable or np array. - :param w: weights - :type w: np.ndarray OR cp.Variable - :param obj: objective function expression - :type obj: cp.Expression - :return: value of the objective function OR objective function expression - :rtype: float OR cp.Expression + Parameters + ---------- + w : np.ndarray or cp.Variable + weights + obj : cp.Expression + objective function expression + + Returns + ------- + float or cp.Expression + value of the objective function OR objective function expression """ if isinstance(w, np.ndarray): if np.isscalar(obj): @@ -60,12 +65,17 @@ def portfolio_variance(w, cov_matrix): """ Calculate the total portfolio variance (i.e square volatility). - :param w: asset weights in the portfolio - :type w: np.ndarray OR cp.Variable - :param cov_matrix: covariance matrix - :type cov_matrix: np.ndarray - :return: value of the objective function OR objective function expression - :rtype: float OR cp.Expression + Parameters + ---------- + w : np.ndarray or cp.Variable + asset weights in the portfolio + cov_matrix : np.ndarray + covariance matrix + + Returns + ------- + float or cp.Expression + value of the objective function OR objective function expression """ variance = cp.quad_form(w, cov_matrix, assume_PSD=True) return _objective_value(w, variance) @@ -75,14 +85,19 @@ def portfolio_return(w, expected_returns, negative=True): """ Calculate the (negative) mean return of a portfolio - :param w: asset weights in the portfolio - :type w: np.ndarray OR cp.Variable - :param expected_returns: expected return of each asset - :type expected_returns: np.ndarray - :param negative: whether quantity should be made negative (so we can minimise) - :type negative: boolean - :return: negative mean return - :rtype: float + Parameters + ---------- + w : np.ndarray or cp.Variable + asset weights in the portfolio + expected_returns : np.ndarray + expected return of each asset + negative : boolean + whether quantity should be made negative (so we can minimise) + + Returns + ------- + float + negative mean return """ sign = -1 if negative else 1 mu = w @ expected_returns @@ -93,20 +108,25 @@ def sharpe_ratio(w, expected_returns, cov_matrix, risk_free_rate=0.0, negative=T """ Calculate the (negative) Sharpe ratio of a portfolio - :param w: asset weights in the portfolio - :type w: np.ndarray OR cp.Variable - :param expected_returns: expected return of each asset - :type expected_returns: np.ndarray - :param cov_matrix: covariance matrix - :type cov_matrix: np.ndarray - :param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.0. - The period of the risk-free rate should correspond to the - frequency of expected returns. - :type risk_free_rate: float, optional - :param negative: whether quantity should be made negative (so we can minimise) - :type negative: boolean - :return: (negative) Sharpe ratio - :rtype: float + Parameters + ---------- + w : np.ndarray or cp.Variable + asset weights in the portfolio + expected_returns : np.ndarray + expected return of each asset + cov_matrix : np.ndarray + covariance matrix + risk_free_rate : float, optional + risk-free rate of borrowing/lending, defaults to 0.0. + The period of the risk-free rate should correspond to the + frequency of expected returns. + negative : boolean + whether quantity should be made negative (so we can minimise) + + Returns + ------- + float + (negative) Sharpe ratio """ mu = w @ expected_returns sigma = cp.sqrt(cp.quad_form(w, cov_matrix, assume_PSD=True)) @@ -125,13 +145,18 @@ def L2_reg(w, gamma=1): ef.add_objective(objective_functions.L2_reg, gamma=2) ef.min_volatility() - :param w: asset weights in the portfolio - :type w: np.ndarray OR cp.Variable - :param gamma: L2 regularisation parameter, defaults to 1. Increase if you want more - non-negligible weights - :type gamma: float, optional - :return: value of the objective function OR objective function expression - :rtype: float OR cp.Expression + Parameters + ---------- + w : np.ndarray or cp.Variable + asset weights in the portfolio + gamma : float, optional + L2 regularisation parameter, defaults to 1. Increase if you want more + non-negligible weights + + Returns + ------- + float or cp.Expression + value of the objective function OR objective function expression """ L2_reg = gamma * cp.sum_squares(w) return _objective_value(w, L2_reg) @@ -141,18 +166,23 @@ def quadratic_utility(w, expected_returns, cov_matrix, risk_aversion, negative=T r""" Quadratic utility function, i.e :math:`\mu - \frac 1 2 \delta w^T \Sigma w`. - :param w: asset weights in the portfolio - :type w: np.ndarray OR cp.Variable - :param expected_returns: expected return of each asset - :type expected_returns: np.ndarray - :param cov_matrix: covariance matrix - :type cov_matrix: np.ndarray - :param risk_aversion: risk aversion coefficient. Increase to reduce risk. - :type risk_aversion: float - :param negative: whether quantity should be made negative (so we can minimise). - :type negative: boolean - :return: value of the objective function OR objective function expression - :rtype: float OR cp.Expression + Parameters + ---------- + w : np.ndarray or cp.Variable + asset weights in the portfolio + expected_returns : np.ndarray + expected return of each asset + cov_matrix : np.ndarray + covariance matrix + risk_aversion : float + risk aversion coefficient. Increase to reduce risk. + negative : boolean + whether quantity should be made negative (so we can minimise). + + Returns + ------- + float or cp.Expression + value of the objective function OR objective function expression """ sign = -1 if negative else 1 mu = w @ expected_returns @@ -171,14 +201,19 @@ def transaction_cost(w, w_prev, k=0.001): and multiply by a given fraction (default to 10bps). This simulates a fixed percentage commission from your broker. - :param w: asset weights in the portfolio - :type w: np.ndarray OR cp.Variable - :param w_prev: previous weights - :type w_prev: np.ndarray - :param k: fractional cost per unit weight exchanged - :type k: float - :return: value of the objective function OR objective function expression - :rtype: float OR cp.Expression + Parameters + ---------- + w : np.ndarray or cp.Variable + asset weights in the portfolio + w_prev : np.ndarray + previous weights + k : float + fractional cost per unit weight exchanged + + Returns + ------- + float or cp.Expression + value of the objective function OR objective function expression """ return _objective_value(w, k * cp.norm(w - w_prev, 1)) @@ -188,14 +223,19 @@ def ex_ante_tracking_error(w, cov_matrix, benchmark_weights): Calculate the (square of) the ex-ante Tracking Error, i.e :math:`(w - w_b)^T \\Sigma (w-w_b)`. - :param w: asset weights in the portfolio - :type w: np.ndarray OR cp.Variable - :param cov_matrix: covariance matrix - :type cov_matrix: np.ndarray - :param benchmark_weights: asset weights in the benchmark - :type benchmark_weights: np.ndarray - :return: value of the objective function OR objective function expression - :rtype: float OR cp.Expression + Parameters + ---------- + w : np.ndarray or cp.Variable + asset weights in the portfolio + cov_matrix : np.ndarray + covariance matrix + benchmark_weights : np.ndarray + asset weights in the benchmark + + Returns + ------- + float or cp.Expression + value of the objective function OR objective function expression """ relative_weights = w - benchmark_weights tracking_error = cp.quad_form(relative_weights, cov_matrix) @@ -206,14 +246,19 @@ def ex_post_tracking_error(w, historic_returns, benchmark_returns): """ Calculate the (square of) the ex-post Tracking Error, i.e :math:`Var(r - r_b)`. - :param w: asset weights in the portfolio - :type w: np.ndarray OR cp.Variable - :param historic_returns: historic asset returns - :type historic_returns: np.ndarray - :param benchmark_returns: historic benchmark returns - :type benchmark_returns: pd.Series or np.ndarray - :return: value of the objective function OR objective function expression - :rtype: float OR cp.Expression + Parameters + ---------- + w : np.ndarray or cp.Variable + asset weights in the portfolio + historic_returns : np.ndarray + historic asset returns + benchmark_returns : pd.Series or np.ndarray + historic benchmark returns + + Returns + ------- + float or cp.Expression + value of the objective function OR objective function expression """ if not isinstance(historic_returns, np.ndarray): historic_returns = np.array(historic_returns) diff --git a/pypfopt/plotting.py b/pypfopt/plotting.py index 4bc3c8fa..c5cdaf45 100644 --- a/pypfopt/plotting.py +++ b/pypfopt/plotting.py @@ -44,12 +44,14 @@ def _plot_io(**kwargs): """ Helper method to optionally save the figure to file. - :param filename: name of the file to save to, defaults to None (doesn't save) - :type filename: str, optional - :param dpi: dpi of figure to save or plot, defaults to 300 - :type dpi: int (between 50-500) - :param showfig: whether to plt.show() the figure, defaults to False - :type showfig: bool, optional + Parameters + ---------- + filename : str, optional + name of the file to save to, defaults to None (doesn't save) + dpi : int, optional + dpi of figure to save or plot. Defaults to 300. Should be between 50 and 500. + showfig : bool, optional + whether to plt.show() the figure, defaults to False """ plt = _import_matplotlib() @@ -69,16 +71,20 @@ def plot_covariance(cov_matrix, plot_correlation=False, show_tickers=True, **kwa Generate a basic plot of the covariance (or correlation) matrix, given a covariance matrix. - :param cov_matrix: covariance matrix - :type cov_matrix: pd.DataFrame or np.ndarray - :param plot_correlation: whether to plot the correlation matrix instead, defaults to False. - :type plot_correlation: bool, optional - :param show_tickers: whether to use tickers as labels (not recommended for large portfolios), - defaults to True - :type show_tickers: bool, optional - - :return: matplotlib axis - :rtype: matplotlib.axes object + Parameters + ---------- + cov_matrix : pd.DataFrame or np.ndarray + covariance matrix + plot_correlation : bool, optional + whether to plot the correlation matrix instead, defaults to False. + show_tickers : bool, optional + whether to use tickers as labels (not recommended for large portfolios), + defaults to True + + Returns + ------- + matplotlib.axes object + matplotlib axis """ plt = _import_matplotlib() @@ -107,17 +113,22 @@ def plot_dendrogram(hrp, ax=None, show_tickers=True, **kwargs): """ Plot the clusters in the form of a dendrogram. - :param hrp: HRPpt object that has already been optimized. - :type hrp: object - :param show_tickers: whether to use tickers as labels (not recommended for large portfolios), - defaults to True - :type show_tickers: bool, optional - :param filename: name of the file to save to, defaults to None (doesn't save) - :type filename: str, optional - :param showfig: whether to plt.show() the figure, defaults to False - :type showfig: bool, optional - :return: matplotlib axis - :rtype: matplotlib.axes object + Parameters + ---------- + hrp : object + HRPpt object that has already been optimized. + show_tickers : bool, optional + whether to use tickers as labels (not recommended for large portfolios), + defaults to True + filename : str, optional + name of the file to save to, defaults to None (doesn't save) + showfig : bool, optional + whether to plt.show() the figure, defaults to False + + Returns + ------- + matplotlib.axes object + matplotlib axis """ plt = _import_matplotlib() @@ -324,29 +335,35 @@ def plot_efficient_frontier( """ Plot the efficient frontier based on either a CLA or EfficientFrontier object. - :param opt: an instantiated optimizer object BEFORE optimising an objective - :type opt: EfficientFrontier or CLA - :param ef_param: [EfficientFrontier] whether to use a range over utility, risk, or return. - Defaults to "return". - :type ef_param: str, one of {"utility", "risk", "return"}. - :param ef_param_range: the range of parameter values for ef_param. - If None, automatically compute a range from min->max return. - :type ef_param_range: np.array or list (recommended to use np.arange or np.linspace) - :param points: number of points to plot, defaults to 100. This is overridden if - an `ef_param_range` is provided explicitly. - :type points: int, optional - :param show_assets: whether we should plot the asset risks/returns also, defaults to True - :type show_assets: bool, optional - :param show_tickers: whether we should annotate each asset with its ticker, defaults to False - :type show_tickers: bool, optional - :param interactive: Switch rendering engine between Plotly and Matplotlib - :type show_tickers: bool, optional - :param filename: name of the file to save to, defaults to None (doesn't save) - :type filename: str, optional - :param showfig: whether to plt.show() the figure, defaults to False - :type showfig: bool, optional - :return: matplotlib axis - :rtype: matplotlib.axes object + Parameters + ---------- + opt : EfficientFrontier or CLA + an instantiated optimizer object BEFORE optimising an objective + ef_param : {'utility', 'risk', 'return'}, optional + [EfficientFrontier] whether to use a range over utility, risk, or return. + Defaults to "return". + ef_param_range : np.array or list, optional + the range of parameter values for ef_param. + If None, automatically compute a range from min->max return. + (recommended to use np.arange or np.linspace) + points : int, optional + number of points to plot, defaults to 100. This is overridden if + an `ef_param_range` is provided explicitly. + show_assets : bool, optional + whether we should plot the asset risks/returns also, defaults to True + show_tickers : bool, optional + whether we should annotate each asset with its ticker, defaults to False + interactive : bool, optional + Switch rendering engine between Plotly and Matplotlib + filename : str, optional + name of the file to save to, defaults to None (doesn't save) + showfig : bool, optional + whether to plt.show() the figure, defaults to False + + Returns + ------- + matplotlib.axes object + matplotlib axis """ plt = _import_matplotlib() @@ -399,12 +416,17 @@ def plot_weights(weights, ax=None, **kwargs): """ Plot the portfolio weights as a horizontal bar chart - :param weights: the weights outputted by any PyPortfolioOpt optimizer - :type weights: {ticker: weight} dict - :param ax: ax to plot to, optional - :type ax: matplotlib.axes - :return: matplotlib axis - :rtype: matplotlib.axes + Parameters + ---------- + weights : {ticker: weight} dict + the weights outputted by any PyPortfolioOpt optimizer + ax : matplotlib.axes, optional + ax to plot to + + Returns + ------- + matplotlib.axes + matplotlib axis """ plt = _import_matplotlib() diff --git a/pypfopt/risk_models.py b/pypfopt/risk_models.py index f526c9db..171ce0d0 100644 --- a/pypfopt/risk_models.py +++ b/pypfopt/risk_models.py @@ -36,10 +36,15 @@ def _is_positive_semidefinite(matrix): Any method that requires inverting the covariance matrix will struggle with a non-positive semidefinite matrix - :param matrix: (covariance) matrix to test - :type matrix: np.ndarray, pd.DataFrame - :return: whether matrix is positive semidefinite - :rtype: bool + Parameters + ---------- + matrix : np.ndarray or pd.DataFrame + (covariance) matrix to test + + Returns + ------- + bool + whether matrix is positive semidefinite """ try: # Significantly more efficient than checking eigenvalues (stackoverflow.com/questions/16266720) @@ -57,13 +62,22 @@ def fix_nonpositive_semidefinite(matrix, fix_method="spectral"): The ``spectral`` method sets negative eigenvalues to zero then rebuilds the matrix, while the ``diag`` method adds a small positive value to the diagonal. - :param matrix: raw covariance matrix (may not be PSD) - :type matrix: pd.DataFrame - :param fix_method: {"spectral", "diag"}, defaults to "spectral" - :type fix_method: str, optional - :raises NotImplementedError: if a method is passed that isn't implemented - :return: positive semidefinite covariance matrix - :rtype: pd.DataFrame + Parameters + ---------- + matrix : pd.DataFrame + raw covariance matrix (may not be PSD) + fix_method : str, optional + {"spectral", "diag"}, defaults to "spectral" + + Raises + ------ + NotImplementedError + if a method is passed that isn't implemented + + Returns + ------- + pd.DataFrame + positive semidefinite covariance matrix """ if _is_positive_semidefinite(matrix): return matrix @@ -104,12 +118,15 @@ def risk_matrix(prices, method="sample_cov", **kwargs): Compute a covariance matrix, using the risk model supplied in the ``method`` parameter. - :param prices: adjusted closing prices of the asset, each row is a date - and each column is a ticker/id. - :type prices: pd.DataFrame - :param returns_data: if true, the first argument is returns instead of prices. - :type returns_data: bool, defaults to False. - :param method: the risk model to use. Should be one of: + Parameters + ---------- + prices : pd.DataFrame + adjusted closing prices of the asset, each row is a date + and each column is a ticker/id. + returns_data : bool, optional + if true, the first argument is returns instead of prices. Defaults to False. + method : str, optional + the risk model to use. Should be one of: - ``sample_cov`` - ``semicovariance`` @@ -120,10 +137,15 @@ def risk_matrix(prices, method="sample_cov", **kwargs): - ``ledoit_wolf_constant_correlation`` - ``oracle_approximating`` - :type method: str, optional - :raises NotImplementedError: if the supplied method is not recognised - :return: annualised sample covariance matrix - :rtype: pd.DataFrame + Raises + ------ + NotImplementedError + if the supplied method is not recognised + + Returns + ------- + pd.DataFrame + annualised sample covariance matrix """ if method == "sample_cov": return sample_cov(prices, **kwargs) @@ -151,18 +173,23 @@ def sample_cov(prices, returns_data=False, frequency=252, log_returns=False, **k """ Calculate the annualised sample covariance matrix of (daily) asset returns. - :param prices: adjusted closing prices of the asset, each row is a date - and each column is a ticker/id. - :type prices: pd.DataFrame - :param returns_data: if true, the first argument is returns instead of prices. - :type returns_data: bool, defaults to False. - :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 log_returns: whether to compute using log returns - :type log_returns: bool, defaults to False - :return: annualised sample covariance matrix - :rtype: pd.DataFrame + Parameters + ---------- + prices : pd.DataFrame + adjusted closing prices of the asset, each row is a date + and each column is a ticker/id. + returns_data : bool, optional + if true, the first argument is returns instead of prices. Defaults to False. + frequency : int, optional + number of time periods in a year, defaults to 252 (the number + of trading days in a year) + log_returns : bool, optional + whether to compute using log returns. Defaults to False. + + Returns + ------- + pd.DataFrame + annualised sample covariance matrix """ if not isinstance(prices, pd.DataFrame): warnings.warn("data is not in a dataframe", RuntimeWarning) @@ -190,22 +217,27 @@ def semicovariance( .. semicov = E([min(r_i - B, 0)] . [min(r_j - B, 0)]) - :param prices: adjusted closing prices of the asset, each row is a date - and each column is a ticker/id. - :type prices: pd.DataFrame - :param returns_data: if true, the first argument is returns instead of prices. - :type returns_data: bool, defaults to False. - :param benchmark: the benchmark return, defaults to the daily risk-free rate, i.e - :math:`1.02^{(1/252)} -1`. - :type benchmark: float - :param frequency: number of time periods in a year, defaults to 252 (the number - of trading days in a year). Ensure that you use the appropriate - benchmark, e.g if ``frequency=12`` use monthly benchmark returns - :type frequency: int, optional - :param log_returns: whether to compute using log returns - :type log_returns: bool, defaults to False - :return: semicovariance matrix - :rtype: pd.DataFrame + Parameters + ---------- + prices : pd.DataFrame + adjusted closing prices of the asset, each row is a date + and each column is a ticker/id. + returns_data : bool, optional + if true, the first argument is returns instead of prices. Defaults to False. + benchmark : float + the benchmark return, defaults to the daily risk-free rate, i.e + :math:`1.02^{(1/252)} -1`. + frequency : int, optional + number of time periods in a year, defaults to 252 (the number + of trading days in a year). Ensure that you use the appropriate + benchmark, e.g if ``frequency=12`` use monthly benchmark returns + log_returns : bool, optional + whether to compute using log returns. Defaults to False. + + Returns + ------- + pd.DataFrame + semicovariance matrix """ if not isinstance(prices, pd.DataFrame): warnings.warn("data is not in a dataframe", RuntimeWarning) @@ -225,14 +257,19 @@ def _pair_exp_cov(X, Y, span=180): """ Calculate the exponential covariance between two timeseries of returns. - :param X: first time series of returns - :type X: pd.Series - :param Y: second time series of returns - :type Y: pd.Series - :param span: the span of the exponential weighting function, defaults to 180 - :type span: int, optional - :return: the exponential covariance between X and Y - :rtype: float + Parameters + ---------- + X : pd.Series + first time series of returns + Y : pd.Series + second time series of returns + span : int, optional + the span of the exponential weighting function, defaults to 180 + + Returns + ------- + float + the exponential covariance between X and Y """ covariation = (X - X.mean()) * (Y - Y.mean()) # Exponentially weight the covariation and take the mean @@ -248,20 +285,25 @@ def exp_cov( Estimate the exponentially-weighted covariance matrix, which gives greater weight to more recent data. - :param prices: adjusted closing prices of the asset, each row is a date - and each column is a ticker/id. - :type prices: pd.DataFrame - :param returns_data: if true, the first argument is returns instead of prices. - :type returns_data: bool, defaults to False. - :param span: the span of the exponential weighting function, defaults to 180 - :type span: int, optional - :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 log_returns: whether to compute using log returns - :type log_returns: bool, defaults to False - :return: annualised estimate of exponential covariance matrix - :rtype: pd.DataFrame + Parameters + ---------- + prices : pd.DataFrame + adjusted closing prices of the asset, each row is a date + and each column is a ticker/id. + returns_data : bool, optional + if true, the first argument is returns instead of prices. Defaults to False. + span : int, optional + the span of the exponential weighting function, defaults to 180 + frequency : int, optional + number of time periods in a year, defaults to 252 (the number + of trading days in a year) + log_returns : bool, optional + whether to compute using log returns. Defaults to False. + + Returns + ------- + pd.DataFrame + annualised estimate of exponential covariance matrix """ if not isinstance(prices, pd.DataFrame): warnings.warn("data is not in a dataframe", RuntimeWarning) @@ -325,10 +367,15 @@ def cov_to_corr(cov_matrix): """ Convert a covariance matrix to a correlation matrix. - :param cov_matrix: covariance matrix - :type cov_matrix: pd.DataFrame - :return: correlation matrix - :rtype: pd.DataFrame + Parameters + ---------- + cov_matrix : pd.DataFrame + covariance matrix + + Returns + ------- + pd.DataFrame + correlation matrix """ if not isinstance(cov_matrix, pd.DataFrame): warnings.warn("cov_matrix is not a dataframe", RuntimeWarning) @@ -343,12 +390,17 @@ def corr_to_cov(corr_matrix, stdevs): """ Convert a correlation matrix to a covariance matrix - :param corr_matrix: correlation matrix - :type corr_matrix: pd.DataFrame - :param stdevs: vector of standard deviations - :type stdevs: array-like - :return: covariance matrix - :rtype: pd.DataFrame + Parameters + ---------- + corr_matrix : pd.DataFrame + correlation matrix + stdevs : array-like + vector of standard deviations + + Returns + ------- + pd.DataFrame + covariance matrix """ if not isinstance(corr_matrix, pd.DataFrame): warnings.warn("corr_matrix is not a dataframe", RuntimeWarning) @@ -374,14 +426,16 @@ class CovarianceShrinkage: def __init__(self, prices, returns_data=False, frequency=252, log_returns=False): """ - :param prices: adjusted closing prices of the asset, each row is a date and each column is a ticker/id. - :type prices: pd.DataFrame - :param returns_data: if true, the first argument is returns instead of prices. - :type returns_data: bool, defaults to False. - :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 log_returns: whether to compute using log returns - :type log_returns: bool, defaults to False + Parameters + ---------- + prices : pd.DataFrame + adjusted closing prices of the asset, each row is a date and each column is a ticker/id. + returns_data : bool, optional + if true, the first argument is returns instead of prices. Defaults to False. + frequency : int, optional + number of time periods in a year, defaults to 252 (the number of trading days in a year) + log_returns : bool, optional + whether to compute using log returns. Defaults to False. """ if not _check_soft_dependencies(["scikit-learn"], severity="none"): raise ImportError( @@ -413,10 +467,15 @@ def _format_and_annualize(self, raw_cov_array): Helper method which annualises the output of shrinkage calculations, and formats the result into a dataframe - :param raw_cov_array: raw covariance matrix of daily returns - :type raw_cov_array: np.ndarray - :return: annualised covariance matrix - :rtype: pd.DataFrame + Parameters + ---------- + raw_cov_array : np.ndarray + raw covariance matrix of daily returns + + Returns + ------- + pd.DataFrame + annualised covariance matrix """ assets = self.X.columns cov = pd.DataFrame(raw_cov_array, index=assets, columns=assets) * self.frequency @@ -428,10 +487,15 @@ def shrunk_covariance(self, delta=0.2): sample variance). This method does not estimate an optimal shrinkage parameter, it requires manual input. - :param delta: shrinkage parameter, defaults to 0.2. - :type delta: float, optional - :return: shrunk sample covariance matrix - :rtype: np.ndarray + Parameters + ---------- + delta : float, optional + shrinkage parameter, defaults to 0.2. + + Returns + ------- + np.ndarray + shrunk sample covariance matrix """ self.delta = delta N = self.S.shape[1] @@ -447,13 +511,22 @@ def ledoit_wolf(self, shrinkage_target="constant_variance"): Calculate the Ledoit-Wolf shrinkage estimate for a particular shrinkage target. - :param shrinkage_target: choice of shrinkage target, either ``constant_variance``, - ``single_factor`` or ``constant_correlation``. Defaults to - ``constant_variance``. - :type shrinkage_target: str, optional - :raises NotImplementedError: if the shrinkage_target is unrecognised - :return: shrunk sample covariance matrix - :rtype: np.ndarray + Parameters + ---------- + shrinkage_target : str, optional + choice of shrinkage target, either ``constant_variance``, + ``single_factor`` or ``constant_correlation``. Defaults to + ``constant_variance``. + + Raises + ------ + NotImplementedError + if the shrinkage_target is unrecognised + + Returns + ------- + np.ndarray + shrunk sample covariance matrix """ if shrinkage_target == "constant_variance": X = np.nan_to_num(self.X.values) @@ -475,8 +548,10 @@ def _ledoit_wolf_single_factor(self): with the Sharpe single-factor matrix as the shrinkage target. See Ledoit and Wolf (2001). - :return: shrunk sample covariance matrix, shrinkage constant - :rtype: np.ndarray, float + Returns + ------- + np.ndarray, float + shrunk sample covariance matrix, shrinkage constant """ X = np.nan_to_num(self.X.values) @@ -530,8 +605,10 @@ def _ledoit_wolf_constant_correlation(self): with the constant correlation matrix as the shrinkage target. See Ledoit and Wolf (2003) - :return: shrunk sample covariance matrix, shrinkage constant - :rtype: np.ndarray, float + Returns + ------- + np.ndarray, float + shrunk sample covariance matrix, shrinkage constant """ X = np.nan_to_num(self.X.values) t, n = np.shape(X) @@ -581,8 +658,10 @@ def oracle_approximating(self): """ Calculate the Oracle Approximating Shrinkage estimate - :return: shrunk sample covariance matrix - :rtype: np.ndarray + Returns + ------- + np.ndarray + shrunk sample covariance matrix """ X = np.nan_to_num(self.X.values) shrunk_cov, self.delta = self.covariance.oas(X) diff --git a/weights.csv b/weights.csv new file mode 100644 index 00000000..bf408642 --- /dev/null +++ b/weights.csv @@ -0,0 +1,20 @@ +GOOG,0.0458 +AAPL,0.06743 +FB,0.2008 +BABA,0.08494 +AMZN,0.03525 +GE,0.0 +AMD,0.0 +WMT,0.0 +BAC,0.0 +GM,0.0 +T,0.0 +UAA,0.0 +SHLD,0.0 +XOM,0.0 +RRC,0.0 +BBY,0.01587 +MA,0.3287 +PFE,0.20394 +JPM,0.0 +SBUX,0.01726 From d8d149889a6e7e1113500a216b2963149b88c355 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 00:41:45 +0000 Subject: [PATCH 3/5] Remove weights.csv test artifact from repo Co-authored-by: fkiraly <7985502+fkiraly@users.noreply.github.com> --- weights.csv | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 weights.csv diff --git a/weights.csv b/weights.csv deleted file mode 100644 index bf408642..00000000 --- a/weights.csv +++ /dev/null @@ -1,20 +0,0 @@ -GOOG,0.0458 -AAPL,0.06743 -FB,0.2008 -BABA,0.08494 -AMZN,0.03525 -GE,0.0 -AMD,0.0 -WMT,0.0 -BAC,0.0 -GM,0.0 -T,0.0 -UAA,0.0 -SHLD,0.0 -XOM,0.0 -RRC,0.0 -BBY,0.01587 -MA,0.3287 -PFE,0.20394 -JPM,0.0 -SBUX,0.01726 From abe0bd550030119f7fabfea1f7a8578dde6bdf5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20Kir=C3=A1ly?= Date: Fri, 27 Feb 2026 09:11:26 +0100 Subject: [PATCH 4/5] Update base_optimizer.py --- pypfopt/base_optimizer.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/pypfopt/base_optimizer.py b/pypfopt/base_optimizer.py index 49de6624..67ec1324 100644 --- a/pypfopt/base_optimizer.py +++ b/pypfopt/base_optimizer.py @@ -8,18 +8,23 @@ """ import collections +from collections.abc import Iterable import copy import json -import warnings -from collections.abc import Iterable from typing import List +import warnings import cvxpy as cp import numpy as np import pandas as pd import scipy.optimize as sco -from . import exceptions, objective_functions +from pypfopt.exceptions import InstantiationError, OptimizationError +from pypfopt.objective_functions import ( + portfolio_return, + portfolio_variance, + sharpe_ratio, +) class BaseOptimizer: @@ -276,14 +281,14 @@ def is_parameter_defined(self, parameter_name: str) -> bool: if param.name() == parameter_name and not is_defined: is_defined = True elif param.name() == parameter_name and is_defined: - raise exceptions.InstantiationError( + raise InstantiationError( "Parameter name defined multiple times" ) return is_defined def update_parameter_value(self, parameter_name: str, new_value: float) -> None: if not self.is_parameter_defined(parameter_name): - raise exceptions.InstantiationError("Parameter has not been defined") + raise InstantiationError("Parameter has not been defined") was_updated = False objective_and_constraints = ( self._constraints + [self._objective] @@ -299,7 +304,7 @@ def update_parameter_value(self, parameter_name: str, new_value: float) -> None: param.value = new_value was_updated = True if not was_updated: - raise exceptions.InstantiationError("Parameter was not updated") + raise InstantiationError("Parameter was not updated") def _solve_cvxpy_opt_problem(self): """ @@ -318,14 +323,14 @@ def _solve_cvxpy_opt_problem(self): self._initial_constraint_ids = {const.id for const in self._constraints} else: if not self._objective.id == self._initial_objective: - raise exceptions.InstantiationError( + raise InstantiationError( "The objective function was changed after the initial optimization. " "Please create a new instance instead." ) constr_ids = {const.id for const in self._constraints} if not constr_ids == self._initial_constraint_ids: - raise exceptions.InstantiationError( + raise InstantiationError( "The constraints were changed after the initial optimization. " "Please create a new instance instead." ) @@ -334,10 +339,10 @@ def _solve_cvxpy_opt_problem(self): ) except (TypeError, cp.DCPError) as e: - raise exceptions.OptimizationError from e + raise OptimizationError from e if self._opt.status not in {"optimal", "optimal_inaccurate"}: - raise exceptions.OptimizationError( + raise OptimizationError( "Solver status: {}".format(self._opt.status) ) self.weights = self._w.value.round(16) + 0.0 # +0.0 removes signed zero @@ -361,7 +366,7 @@ def L1_norm(w, k=1): the objective to be added (i.e function of cp.Variable) """ if self._opt is not None: - raise exceptions.InstantiationError( + raise InstantiationError( "Adding objectives to an already solved problem might have unintended consequences. " "A new instance should be created for the new set of objectives." ) @@ -388,7 +393,7 @@ def add_constraint(self, new_constraint): "New constraint must be provided as a callable (e.g lambda function)" ) if self._opt is not None: - raise exceptions.InstantiationError( + raise InstantiationError( "Adding constraints to an already solved problem might have unintended consequences. " "A new instance should be created for the new set of constraints." ) @@ -607,14 +612,14 @@ def portfolio_performance( else: raise ValueError("Weights is None") - sigma = np.sqrt(objective_functions.portfolio_variance(new_weights, cov_matrix)) + sigma = np.sqrt(portfolio_variance(new_weights, cov_matrix)) if expected_returns is not None: - mu = objective_functions.portfolio_return( + mu = portfolio_return( new_weights, expected_returns, negative=False ) - sharpe = objective_functions.sharpe_ratio( + sharpe = sharpe_ratio( new_weights, expected_returns, cov_matrix, From 27aa47208df5a9473df2bddb98d7c266808754af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz=20Kir=C3=A1ly?= Date: Fri, 27 Feb 2026 09:13:47 +0100 Subject: [PATCH 5/5] Update base_optimizer.py --- pypfopt/base_optimizer.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pypfopt/base_optimizer.py b/pypfopt/base_optimizer.py index 67ec1324..ad8c771b 100644 --- a/pypfopt/base_optimizer.py +++ b/pypfopt/base_optimizer.py @@ -281,9 +281,7 @@ def is_parameter_defined(self, parameter_name: str) -> bool: if param.name() == parameter_name and not is_defined: is_defined = True elif param.name() == parameter_name and is_defined: - raise InstantiationError( - "Parameter name defined multiple times" - ) + raise InstantiationError("Parameter name defined multiple times") return is_defined def update_parameter_value(self, parameter_name: str, new_value: float) -> None: @@ -342,9 +340,7 @@ def _solve_cvxpy_opt_problem(self): raise OptimizationError from e if self._opt.status not in {"optimal", "optimal_inaccurate"}: - raise OptimizationError( - "Solver status: {}".format(self._opt.status) - ) + raise OptimizationError("Solver status: {}".format(self._opt.status)) self.weights = self._w.value.round(16) + 0.0 # +0.0 removes signed zero return self._make_output_weights() @@ -615,9 +611,7 @@ def portfolio_performance( sigma = np.sqrt(portfolio_variance(new_weights, cov_matrix)) if expected_returns is not None: - mu = portfolio_return( - new_weights, expected_returns, negative=False - ) + mu = portfolio_return(new_weights, expected_returns, negative=False) sharpe = sharpe_ratio( new_weights,