Skip to content

Commit 746abab

Browse files
committed
reorganised docs
1 parent 72b8bdb commit 746abab

15 files changed

+484
-478
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,8 +330,10 @@ ef.max_sharpe()
330330

331331
### Other optimisers
332332

333-
The features above mostly pertain to solving efficient frontier optimisation problems via quadratic programming (though this is taken care of by `cvxpy`). However, we offer different optimisers as well:
333+
The features above mostly pertain to solving mean-variance optimisation problems via quadratic programming (though this is taken care of by `cvxpy`). However, we offer different optimisers as well:
334334

335+
- Mean-semivariance optimisation
336+
- Mean-CVaR optimisation
335337
- Hierarchical Risk Parity, using clustering algorithms to choose uncorrelated assets
336338
- Markowitz's critical line algorithm (CLA)
337339

docs/EfficientFrontier.rst

Lines changed: 0 additions & 367 deletions
This file was deleted.

docs/ExpectedReturns.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ superior models and feed them into the optimiser.
1414

1515
.. caution::
1616

17-
In my experience, supplying expected returns often does more harm than good. If
17+
Supplying expected returns can do more harm than good. If
1818
predicting stock returns were as easy as calculating the mean historical return,
1919
we'd all be rich! For most use-cases, I would suggest that you focus your efforts
2020
on choosing an appropriate risk model (see :ref:`risk-models`).
2121

22-
As of v0.5.0, you can use :ref:`black-litterman` to greatly improve the quality of
22+
As of v0.5.0, you can use :ref:`black-litterman` to significantly improve the quality of
2323
your estimate of the expected returns.
2424

2525
.. automodule:: pypfopt.expected_returns
@@ -33,7 +33,7 @@ superior models and feed them into the optimiser.
3333

3434
This is probably the default textbook approach. It is intuitive and easily interpretable,
3535
however the estimates are subject to large uncertainty. This is a problem especially in the
36-
context of a quadratic optimiser, which will maximise the erroneous inputs.
36+
context of a mean-variance optimiser, which will maximise the erroneous inputs.
3737

3838

3939
.. autofunction:: ema_historical_return

docs/GeneralEfficientFrontier.rst

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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

Comments
 (0)