|
| 1 | +.. _efficient-frontier: |
| 2 | + |
| 3 | +########################## |
| 4 | +General Efficient Frontier |
| 5 | +########################## |
| 6 | + |
| 7 | +The mean-variance optimisation methods described previously can be used whenever you have a vector |
| 8 | +of expected returns and a covariance matrix. The objective and constraints will be some combination |
| 9 | +of the portfolio return and portfolio volatility. |
| 10 | + |
| 11 | +However, you may want to construct the efficient frontier for an entirely different type of risk model |
| 12 | +(one that doesn't depend on covariance matrices), or optimise an objective unrelated to portfolio |
| 13 | +return (e.g tracking error). PyPortfolioOpt comes with several popular alternatives and provides support |
| 14 | +for custom optimisation problems. |
| 15 | + |
| 16 | +Efficient Semivariance |
| 17 | +====================== |
| 18 | + |
| 19 | +Instead of penalising volatility, mean-semivariance optimisation seeks to only penalise |
| 20 | +downside volatility, since upside volatility may be desirable. |
| 21 | + |
| 22 | +There are two approaches to the mean-semivariance optimisation problem. The first is to use a |
| 23 | +heuristic (i.e "quick and dirty") solution: pretending that the semicovariance matrix |
| 24 | +(implemented in :py:mod:`risk_models`) is a typical covariance matrix and doing standard |
| 25 | +mean-variance optimisation. It can be shown that this *does not* yield a portfolio that |
| 26 | +is efficient in mean-semivariance space (though it might be a good-enough approximation). |
| 27 | + |
| 28 | +Fortunately, it is possible to write mean-semivariance optimisation as a convex problem |
| 29 | +(albeit one with many variables), that can be solved to give an "exact" solution. |
| 30 | +For example, to maximise return for a target semivariance |
| 31 | +:math:`s^*` (long-only), we would solve the following problem: |
| 32 | + |
| 33 | +.. math:: |
| 34 | +
|
| 35 | + \begin{equation*} |
| 36 | + \begin{aligned} |
| 37 | + & \underset{w}{\text{maximise}} & & w^T \mu \\ |
| 38 | + & \text{subject to} & & n^T n \leq s^* \\ |
| 39 | + &&& B w - p + n = 0 \\ |
| 40 | + &&& w^T \mathbf{1} = 1 \\ |
| 41 | + &&& n \geq 0 \\ |
| 42 | + &&& p \geq 0. \\ |
| 43 | + \end{aligned} |
| 44 | + \end{equation*} |
| 45 | +
|
| 46 | +Here, **B** is the :math:`T \times N` (scaled) matrix of excess returns: |
| 47 | +``B = (returns - benchmark) / sqrt(T)``. Additional linear equality constraints and |
| 48 | +convex inequality constraints can be added. |
| 49 | + |
| 50 | +PyPortfolioOpt allows users to optimise along the efficient semivariance frontier |
| 51 | +via the :py:class:`EfficientSemivariance` class. :py:class:`EfficientSemivariance` inherits from |
| 52 | +:py:class:`EfficientFrontier`, so it has the same utility methods |
| 53 | +(e.g :py:func:`add_constraint`, :py:func:`portfolio_performance`), but finds portfolios on the mean-semivariance |
| 54 | +frontier. Note that some of the parent methods, like :py:func:`max_sharpe` and :py:func:`min_volatility` |
| 55 | +are not applicable to mean-semivariance portfolios, so calling them returns ``NotImplementedError``. |
| 56 | + |
| 57 | +:py:class:`EfficientSemivariance` has a slightly different API to :py:class:`EfficientFrontier`. Instead of passing |
| 58 | +in a covariance matrix, you should past in a dataframe of historical/simulated returns (this can be constructed |
| 59 | +from your price dataframe using the helper method :py:func:`expected_returns.returns_from_prices`). Here |
| 60 | +is a full example, in which we seek the portfolio that minimises the semivariance for a target |
| 61 | +annual return of 20%:: |
| 62 | + |
| 63 | + from pypfopt import expected_returns, EfficientSemivariance |
| 64 | + |
| 65 | + df = ... # your dataframe of prices |
| 66 | + mu = expected_returns.mean_historical_returns(df) |
| 67 | + historical_returns = expected_returns.returns_from_prices(df) |
| 68 | + |
| 69 | + es = EfficientSemivariance(mu, historical_returns) |
| 70 | + es.efficient_return(0.20) |
| 71 | + |
| 72 | + # We can use the same helper methods as before |
| 73 | + weights = es.clean_weights() |
| 74 | + print(weights) |
| 75 | + es.portfolio_performance(verbose=True) |
| 76 | + |
| 77 | +The ``portfolio_performance`` method outputs the expected portfolio return, semivariance, |
| 78 | +and the Sortino ratio (like the Sharpe ratio, but for downside deviation). |
| 79 | + |
| 80 | +Interested readers should refer to Estrada (2007) [1]_ for more details. I'd like to thank |
| 81 | +`Philipp Schiele <https://github.com/phschiele>`_ for authoring the bulk |
| 82 | +of the efficient semivariance functionality and documentation (all errors are my own). The |
| 83 | +implementation is based on Markowitz et al (2019) [2]_. |
| 84 | + |
| 85 | +.. caution:: |
| 86 | + |
| 87 | + Finding portfolios on the mean-semivariance frontier is computationally harder |
| 88 | + than standard mean-variance optimisation: our implementation uses ``2T + N`` optimisation variables, |
| 89 | + meaning that for 50 assets and 3 years of data, there are about 1500 variables. |
| 90 | + While :py:class:`EfficientSemivariance` allows for additional constraints/objectives in principle, |
| 91 | + you are much more likely to run into solver errors. I suggest that you keep :py:class:`EfficientSemivariance` |
| 92 | + problems small and minimally constrained. |
| 93 | + |
| 94 | +.. autoclass:: pypfopt.efficient_frontier.EfficientSemivariance |
| 95 | + :members: |
| 96 | + :exclude-members: max_sharpe, min_volatility |
| 97 | + |
| 98 | +Efficient CVaR |
| 99 | +============== |
| 100 | + |
| 101 | +The **conditional value-at-risk** (a.k.a **expected shortfall**) is a popular measure of tail risk. The CVaR can be |
| 102 | +thought of as the average of losses that occur on "very bad days", where "very bad" is quantified by the parameter |
| 103 | +:math:`\beta`. |
| 104 | + |
| 105 | +For example, if we calculate the CVaR to be 10% for :math:`\beta = 0.95`, we can be 95% confident that the worst-case |
| 106 | +average daily loss will be 3%. Put differently, the CVaR is the average of all losses so severe that they only occur |
| 107 | +:math:`(1-\beta)\%` of the time. |
| 108 | + |
| 109 | +While CVaR is quite an intuitive concept, a lot of new notation is required to formulate it mathematically (see |
| 110 | +the `wiki page <https://en.wikipedia.org/wiki/Expected_shortfall>`_ for more details). We will adopt the following |
| 111 | +notation: |
| 112 | + |
| 113 | +- *w* for the vector of portfolio weights |
| 114 | +- *r* for a vector of asset returns (daily), with probability distribution :math:`p(r)`. |
| 115 | +- :math:`L(w, r) = - w^T r` for the loss of the portfolio |
| 116 | +- :math:`\alpha` for the portfolio value-at-risk (VaR) with confidence :math:`\beta`. |
| 117 | + |
| 118 | +The CVaR can then be written as: |
| 119 | + |
| 120 | +.. math:: |
| 121 | + CVaR(w, \beta) = \frac{1}{1-\beta} \int_{L(w, r) \geq \alpha (w)} L(w, r) p(r)dr. |
| 122 | +
|
| 123 | +This is a nasty expression to optimise because we are essentially integrating over VaR values. The key insight |
| 124 | +of Rockafellar and Uryasev (2001) [3]_ is that we can can equivalently optimise the following convex function: |
| 125 | + |
| 126 | +.. math:: |
| 127 | + F_\beta (w, \alpha) = \alpha + \frac{1}{1-\beta} \int [-w^T r - \alpha]^+ p(r) dr, |
| 128 | +
|
| 129 | +where :math:`[x]^+ = \max(x, 0)`. The authors prove that minimising :math:`F_\beta(w, \alpha)` over all |
| 130 | +:math:`w, \alpha` minimises the CVaR. Suppose we have a sample of *T* daily returns (these |
| 131 | +can either be historical or simulated). The integral in the expression becomes a sum, so the CVaR optimisation |
| 132 | +problem reduces to a linear program: |
| 133 | + |
| 134 | +.. math:: |
| 135 | +
|
| 136 | + \begin{equation*} |
| 137 | + \begin{aligned} |
| 138 | + & \underset{w, \alpha}{\text{minimise}} & & \alpha + \frac{1}{1-\beta} \frac 1 T \sum_{i=1}^T u_i \\ |
| 139 | + & \text{subject to} & & u_i \geq 0 \\ |
| 140 | + &&& u_i \geq -w^T r_i - \alpha. \\ |
| 141 | + \end{aligned} |
| 142 | + \end{equation*} |
| 143 | +
|
| 144 | +This formulation introduces a new variable for each datapoint (similar to Efficient Semivariance), so |
| 145 | +you may run into performance issues for long returns dataframes. At the same time, you should aim to |
| 146 | +provide a sample of data that is large enough to include tail events. |
| 147 | + |
| 148 | +I am grateful to `Nicolas Knudde <https://github.com/nknudde>`_ for the initial draft (all errors are my own). |
| 149 | +The implementation is based on Rockafellar and Uryasev (2001) [3]_. |
| 150 | + |
| 151 | + |
| 152 | +.. autoclass:: pypfopt.efficient_frontier.EfficientCVaR |
| 153 | + :members: |
| 154 | + :exclude-members: max_sharpe, min_volatility, max_quadratic_utility |
| 155 | + |
| 156 | + |
| 157 | +.. _custom-optimisation: |
| 158 | + |
| 159 | +Custom optimisation problems |
| 160 | +============================ |
| 161 | + |
| 162 | +We have seen previously that it is easy to add constraints to ``EfficientFrontier`` objects (and |
| 163 | +by extension, other general efficient frontier objects like ``EfficientSemivariance``). However, what if you aren't interested |
| 164 | +in anything related to ``max_sharpe()``, ``min_volatility()``, ``efficient_risk()`` etc and want to |
| 165 | +set up a completely new problem to optimise for some custom objective? |
| 166 | + |
| 167 | +For example, perhaps our objective is to construct a basket of assets that best replicates a |
| 168 | +particular index, in otherwords, to minimise the **tracking error**. This does not fit within |
| 169 | +a mean-variance optimisation paradigm, but we can still implement it in PyPortfolioOpt:: |
| 170 | + |
| 171 | + from pypfopt.base_optimizer import BaseConvexOptimizer |
| 172 | + from pypfopt.objective_functions import ex_post_tracking error |
| 173 | + |
| 174 | + historic_returns = ... # dataframe of historic asset returns |
| 175 | + S = risk_models.sample_cov(historic_returns, returns_data=True) |
| 176 | + |
| 177 | + opt = BaseConvexOptimizer( |
| 178 | + n_assets=len(historic_returns.columns), |
| 179 | + tickers=historic_returns.index, |
| 180 | + weight_bounds=(0, 1) |
| 181 | + ) |
| 182 | + opt.convex_objective( |
| 183 | + objective_functions.ex_post_tracking_error, |
| 184 | + historic_returns=historical_rets, |
| 185 | + benchmark_returns=benchmark_rets, |
| 186 | + ) |
| 187 | + weights = opt.clean_weights() |
| 188 | + |
| 189 | +The ``EfficientFrontier`` class inherits from ``BaseConvexOptimizer``. It may be more convenient |
| 190 | +to call ``convex_objective`` from an ``EfficientFrontier`` instance than from ``BaseConvexOptimizer``, |
| 191 | +particularly if your objective depends on the mean returns or covariance matrix. |
| 192 | + |
| 193 | +You can either optimise some generic ``convex_objective`` |
| 194 | +(which *must* be built using ``cvxpy`` atomic functions -- see `here <https://www.cvxpy.org/tutorial/functions/index.html>`_) |
| 195 | +or a ``nonconvex_objective``, which uses ``scipy.optimize`` as the backend and thus has a completely |
| 196 | +different API. For more examples, check out this `cookbook recipe |
| 197 | +<https://github.com/robertmartin8/PyPortfolioOpt/blob/master/cookbook/3-Advanced-Mean-Variance-Optimisation.ipynb>`_. |
| 198 | + |
| 199 | + .. class:: pypfopt.base_optimizer.BaseConvexOptimizer |
| 200 | + |
| 201 | + .. automethod:: convex_objective |
| 202 | + |
| 203 | + .. automethod:: nonconvex_objective |
| 204 | + |
| 205 | + |
| 206 | + |
| 207 | +References |
| 208 | +========== |
| 209 | + |
| 210 | +.. [1] Estrada, J (2007). `Mean-Semivariance Optimization: A Heuristic Approach <https://papers.ssrn.com/sol3/papers.cfm?abstract_id=1028206>`_. |
| 211 | +.. [2] Markowitz, H.; Starer, D.; Fram, H.; Gerber, S. (2019). `Avoiding the Downside <https://www.hudsonbaycapital.com/documents/FG/hudsonbay/research/599440_paper.pdf>`_. |
| 212 | +.. [3] Rockafellar, R.; Uryasev, D. (2001). `Optimization of conditional value-at-risk <https://www.ise.ufl.edu/uryasev/files/2011/11/CVaR1_JOR.pdf>`_ |
0 commit comments