Merge branch 'v1.2.5' into master

This commit is contained in:
Robert Martin
2020-08-31 19:59:21 +08:00
committed by GitHub
26 changed files with 545 additions and 612 deletions

View File

@@ -203,7 +203,6 @@ As of v1.2.0:
- Introduced a new API, in which the function `expected_returns.return_model(method="...")` allows
all the different return models to be called. This should make testing easier.
- Added option to 'properly' compound returns.
- James-Stein shrinkage estimator
- CAPM return model.
- `from pypfopt import plotting`: moved all plotting functionality into a new class and added
new plots. All other plotting functions (scattered in different classes) have been retained,
@@ -244,9 +243,6 @@ A far more comprehensive version of this can be found on [ReadTheDocs](https://p
- Exponentially weighted mean historical returns:
- similar to mean historical returns, except it gives exponentially more weight to recent prices
- it is likely the case that an asset's most recent returns hold more weight than returns from 10 years ago when it comes to estimating future returns.
- James-Stein shrinkage:
- a slightly more robust estimate of future returns
- by shrinking mean returns to the grand average, we can reduce loss.
- Capital Asset Pricing Model (CAPM):
- a simple model to predict returns based on the beta to the market
- this is used all over finance!

File diff suppressed because one or more lines are too long

View File

@@ -1,88 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Hierarchical Risk Parity Portfolio\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"from pypfopt import HRPOpt, risk_models, plotting, DiscreteAllocation\n",
"\n",
"\n",
"df = pd.read_csv(\"tests/stock_prices.csv\", parse_dates=True, index_col=\"date\")\n",
"print(df.shape)\n",
"df.tail()\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"returns = risk_models.returns_from_prices(df)\n",
"hrp = HRPOpt(returns)\n",
"weights = hrp.optimize()\n",
"hrp.portfolio_performance(verbose=True);"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"plotting.plot_dendrogram(hrp);"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"da = DiscreteAllocation(weights, df.iloc[-1], total_portfolio_value=10000)\n",
"w, leftover = da.lp_portfolio()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"plotting.plot_weights(weights);"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.5"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@@ -46,20 +46,6 @@ superior models and feed them into the optimiser.
the mean historical return. However, if you plan on rebalancing much more frequently,
there is a case to be made for lowering the span in order to capture recent trends.
.. autofunction:: james_stein_shrinkage
A surprising result in statistics is that the MLE estimator for a 3+ dimensional
Normal distribution is an *inadmissible* estimator. That is, there exists an estimator
:math:`\hat{\mu}^{JS}` such that:
.. math::
E\{ \lVert \hat{\mu}^{JS} - \mu \rVert^2 \} < E \{ \lVert \bar{\mu} - \mu \rVert^2 \}
In essence, to reduce account for the fact that our sample may not be representative and
thus reduce loss, we shrink the sample means to the "grand average" (mean of means). For
a more detailed explanation, refer to Efron and Hastie (2010) [1]_
.. autofunction:: capm_return
.. autofunction:: returns_from_prices
@@ -69,7 +55,5 @@ superior models and feed them into the optimiser.
.. autofunction:: prices_from_returns
References
==========
.. [1] Efron and Hastie (2010) `Empirical Bayes and the JamesStein Estimator <http://statweb.stanford.edu/~ckirby/brad/LSI/chapter1.pdf>`_.
.. References
.. ==========

View File

@@ -38,7 +38,7 @@ Sharpe Ratio: 1.09
# Black-Litterman
spy_prices = pd.read_csv(
"tests/spy_prices.csv", parse_dates=True, index_col=0, squeeze=True
"tests/resources/spy_prices.csv", parse_dates=True, index_col=0, squeeze=True
)
delta = black_litterman.market_implied_risk_aversion(spy_prices)

359
poetry.lock generated
View File

@@ -16,6 +16,23 @@ optional = false
python-versions = "*"
version = "0.1.0"
[[package]]
category = "dev"
description = "The secure Argon2 password hashing algorithm."
name = "argon2-cffi"
optional = false
python-versions = "*"
version = "20.1.0"
[package.dependencies]
cffi = ">=1.0.0"
six = "*"
[package.extras]
dev = ["coverage (>=5.0.2)", "hypothesis", "pytest", "sphinx", "wheel", "pre-commit"]
docs = ["sphinx"]
tests = ["coverage (>=5.0.2)", "hypothesis", "pytest"]
[[package]]
category = "dev"
description = "Atomic file writes."
@@ -30,13 +47,12 @@ description = "Classes Without Boilerplate"
name = "attrs"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "19.3.0"
version = "20.1.0"
[package.extras]
azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"]
dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"]
docs = ["sphinx", "zope.interface"]
tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"]
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
[[package]]
category = "dev"
@@ -44,7 +60,7 @@ description = "Specifications for callback functions passed in to an API"
name = "backcall"
optional = false
python-versions = "*"
version = "0.1.0"
version = "0.2.0"
[[package]]
category = "dev"
@@ -77,6 +93,17 @@ packaging = "*"
six = ">=1.9.0"
webencodings = "*"
[[package]]
category = "dev"
description = "Foreign Function Interface for Python calling C code."
name = "cffi"
optional = false
python-versions = "*"
version = "1.14.2"
[package.dependencies]
pycparser = "*"
[[package]]
category = "dev"
description = "Composable command line interface toolkit"
@@ -95,21 +122,28 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.4.3"
[[package]]
category = "main"
description = "Convex optimization package"
name = "cvxopt"
optional = false
python-versions = "*"
version = "1.2.5"
[[package]]
category = "main"
description = "A domain-specific language for modeling convex optimization problems in Python."
name = "cvxpy"
optional = false
python-versions = "*"
version = "1.0.31"
python-versions = ">=3.5"
version = "1.1.5"
[package.dependencies]
ecos = ">=2"
multiprocess = "*"
numpy = ">=1.15"
osqp = ">=0.4.1"
scipy = ">=1.1.0"
scs = ">=1.1.3"
scs = ">=1.1.5"
[[package]]
category = "dev"
@@ -127,17 +161,6 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.6.0"
[[package]]
category = "main"
description = "serialize all of python"
name = "dill"
optional = false
python-versions = ">=2.6, !=3.0.*"
version = "0.3.1.1"
[package.extras]
graph = ["objgraph (>=1.7.2)"]
[[package]]
category = "main"
description = "This is the Python package for ECOS: Embedded Cone Solver. See Github page for more information."
@@ -164,7 +187,7 @@ description = "the modular source code checker: pep8 pyflakes and co"
name = "flake8"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
version = "3.8.2"
version = "3.8.3"
[package.dependencies]
mccabe = ">=0.6.0,<0.7.0"
@@ -190,7 +213,7 @@ marker = "python_version < \"3.8\""
name = "importlib-metadata"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
version = "1.6.1"
version = "1.7.0"
[package.dependencies]
zipp = ">=0.5"
@@ -205,7 +228,7 @@ description = "IPython Kernel for Jupyter"
name = "ipykernel"
optional = false
python-versions = ">=3.5"
version = "5.3.0"
version = "5.3.4"
[package.dependencies]
appnope = "*"
@@ -288,14 +311,14 @@ description = "An autocompletion tool for Python that can be used for text edito
name = "jedi"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.17.0"
version = "0.17.2"
[package.dependencies]
parso = ">=0.7.0"
parso = ">=0.7.0,<0.8.0"
[package.extras]
qa = ["flake8 (3.7.9)"]
testing = ["colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"]
testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"]
[[package]]
category = "dev"
@@ -355,7 +378,7 @@ description = "Jupyter protocol implementation and client libraries"
name = "jupyter-client"
optional = false
python-versions = ">=3.5"
version = "6.1.3"
version = "6.1.7"
[package.dependencies]
jupyter-core = ">=4.6.0"
@@ -365,7 +388,7 @@ tornado = ">=4.1"
traitlets = "*"
[package.extras]
test = ["ipykernel", "ipython", "mock", "pytest"]
test = ["ipykernel", "ipython", "mock", "pytest", "pytest-asyncio", "async-generator", "pytest-timeout"]
[[package]]
category = "dev"
@@ -428,18 +451,7 @@ marker = "python_version > \"2.7\""
name = "more-itertools"
optional = false
python-versions = ">=3.5"
version = "8.3.0"
[[package]]
category = "main"
description = "better multiprocessing and multithreading in python"
name = "multiprocess"
optional = false
python-versions = "*"
version = "0.70.9"
[package.dependencies]
dill = ">=0.3.1"
version = "8.5.0"
[[package]]
category = "dev"
@@ -475,7 +487,7 @@ description = "The Jupyter Notebook format"
name = "nbformat"
optional = false
python-versions = ">=3.5"
version = "5.0.6"
version = "5.0.7"
[package.dependencies]
ipython-genutils = "*"
@@ -484,7 +496,7 @@ jupyter-core = "*"
traitlets = ">=4.1"
[package.extras]
test = ["testpath", "pytest", "pytest-cov"]
test = ["pytest", "pytest-cov", "testpath"]
[[package]]
category = "dev"
@@ -492,10 +504,11 @@ description = "A web-based notebook environment for interactive computing"
name = "notebook"
optional = false
python-versions = ">=3.5"
version = "6.0.3"
version = "6.1.3"
[package.dependencies]
Send2Trash = "*"
argon2-cffi = "*"
ipykernel = "*"
ipython-genutils = "*"
jinja2 = "*"
@@ -505,12 +518,13 @@ nbconvert = "*"
nbformat = "*"
prometheus-client = "*"
pyzmq = ">=17"
terminado = ">=0.8.1"
terminado = ">=0.8.3"
tornado = ">=5.0"
traitlets = ">=4.2.1"
[package.extras]
test = ["nose", "coverage", "requests", "nose-warnings-filters", "nbval", "nose-exclude", "selenium", "pytest", "pytest-cov", "nose-exclude"]
docs = ["sphinx", "nbsphinx", "sphinxcontrib-github-alt"]
test = ["nose", "coverage", "requests", "nose-warnings-filters", "nbval", "nose-exclude", "selenium", "pytest", "pytest-cov", "requests-unixsocket"]
[[package]]
category = "main"
@@ -572,7 +586,7 @@ description = "A Python Parser"
name = "parso"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.7.0"
version = "0.7.1"
[package.extras]
testing = ["docopt", "pytest (>=3.0.7)"]
@@ -663,7 +677,7 @@ description = "library with cross-python path, ini-parsing, io, code, log facili
name = "py"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.8.1"
version = "1.9.0"
[[package]]
category = "dev"
@@ -673,6 +687,14 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.6.0"
[[package]]
category = "dev"
description = "C parser in Python"
name = "pycparser"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.20"
[[package]]
category = "dev"
description = "passive checker of Python programs"
@@ -770,7 +792,7 @@ marker = "sys_platform == \"win32\""
name = "pywin32"
optional = false
python-versions = "*"
version = "227"
version = "228"
[[package]]
category = "dev"
@@ -787,7 +809,7 @@ description = "Python bindings for 0MQ"
name = "pyzmq"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*"
version = "19.0.1"
version = "19.0.2"
[[package]]
category = "dev"
@@ -795,7 +817,7 @@ description = "Jupyter Qt console"
name = "qtconsole"
optional = false
python-versions = "*"
version = "4.7.4"
version = "4.7.6"
[package.dependencies]
ipykernel = ">=4.1"
@@ -929,7 +951,7 @@ description = "Measures the displayed width of unicode strings in a terminal"
name = "wcwidth"
optional = false
python-versions = "*"
version = "0.2.3"
version = "0.2.5"
[[package]]
category = "dev"
@@ -977,7 +999,7 @@ matplotlib = []
scikit-learn = []
[metadata]
content-hash = "d25a25d8c927137e2fb52a061ae18663175879217a6a4a632b4c3e0b166acc55"
content-hash = "0ec98fc57e383a229e29979c086e8fc1417f6654529c9441afa00209c19f012d"
python-versions = "^3.5"
[metadata.files]
@@ -989,17 +1011,35 @@ appnope = [
{file = "appnope-0.1.0-py2.py3-none-any.whl", hash = "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0"},
{file = "appnope-0.1.0.tar.gz", hash = "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"},
]
argon2-cffi = [
{file = "argon2-cffi-20.1.0.tar.gz", hash = "sha256:d8029b2d3e4b4cea770e9e5a0104dd8fa185c1724a0f01528ae4826a6d25f97d"},
{file = "argon2_cffi-20.1.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:6ea92c980586931a816d61e4faf6c192b4abce89aa767ff6581e6ddc985ed003"},
{file = "argon2_cffi-20.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:05a8ac07c7026542377e38389638a8a1e9b78f1cd8439cd7493b39f08dd75fbf"},
{file = "argon2_cffi-20.1.0-cp27-cp27m-win32.whl", hash = "sha256:0bf066bc049332489bb2d75f69216416329d9dc65deee127152caeb16e5ce7d5"},
{file = "argon2_cffi-20.1.0-cp27-cp27m-win_amd64.whl", hash = "sha256:57358570592c46c420300ec94f2ff3b32cbccd10d38bdc12dc6979c4a8484fbc"},
{file = "argon2_cffi-20.1.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7d455c802727710e9dfa69b74ccaab04568386ca17b0ad36350b622cd34606fe"},
{file = "argon2_cffi-20.1.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:b160416adc0f012fb1f12588a5e6954889510f82f698e23ed4f4fa57f12a0647"},
{file = "argon2_cffi-20.1.0-cp35-cp35m-win32.whl", hash = "sha256:9bee3212ba4f560af397b6d7146848c32a800652301843df06b9e8f68f0f7361"},
{file = "argon2_cffi-20.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:392c3c2ef91d12da510cfb6f9bae52512a4552573a9e27600bdb800e05905d2b"},
{file = "argon2_cffi-20.1.0-cp36-cp36m-win32.whl", hash = "sha256:ba7209b608945b889457f949cc04c8e762bed4fe3fec88ae9a6b7765ae82e496"},
{file = "argon2_cffi-20.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:da7f0445b71db6d3a72462e04f36544b0de871289b0bc8a7cc87c0f5ec7079fa"},
{file = "argon2_cffi-20.1.0-cp37-abi3-macosx_10_6_intel.whl", hash = "sha256:cc0e028b209a5483b6846053d5fd7165f460a1f14774d79e632e75e7ae64b82b"},
{file = "argon2_cffi-20.1.0-cp37-cp37m-win32.whl", hash = "sha256:18dee20e25e4be86680b178b35ccfc5d495ebd5792cd00781548d50880fee5c5"},
{file = "argon2_cffi-20.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6678bb047373f52bcff02db8afab0d2a77d83bde61cfecea7c5c62e2335cb203"},
{file = "argon2_cffi-20.1.0-cp38-cp38-win32.whl", hash = "sha256:77e909cc756ef81d6abb60524d259d959bab384832f0c651ed7dcb6e5ccdbb78"},
{file = "argon2_cffi-20.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:9dfd5197852530294ecb5795c97a823839258dfd5eb9420233c7cfedec2058f2"},
]
atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [
{file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
{file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"},
{file = "attrs-20.1.0-py2.py3-none-any.whl", hash = "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff"},
{file = "attrs-20.1.0.tar.gz", hash = "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a"},
]
backcall = [
{file = "backcall-0.1.0.tar.gz", hash = "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4"},
{file = "backcall-0.1.0.zip", hash = "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"},
{file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
{file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
]
black = [
{file = "black-18.9b0-py36-none-any.whl", hash = "sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739"},
@@ -1009,6 +1049,36 @@ bleach = [
{file = "bleach-3.1.5-py2.py3-none-any.whl", hash = "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f"},
{file = "bleach-3.1.5.tar.gz", hash = "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b"},
]
cffi = [
{file = "cffi-1.14.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82"},
{file = "cffi-1.14.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4"},
{file = "cffi-1.14.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e"},
{file = "cffi-1.14.2-cp27-cp27m-win32.whl", hash = "sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c"},
{file = "cffi-1.14.2-cp27-cp27m-win_amd64.whl", hash = "sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1"},
{file = "cffi-1.14.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7"},
{file = "cffi-1.14.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c"},
{file = "cffi-1.14.2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731"},
{file = "cffi-1.14.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0"},
{file = "cffi-1.14.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e"},
{file = "cffi-1.14.2-cp35-cp35m-win32.whl", hash = "sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487"},
{file = "cffi-1.14.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad"},
{file = "cffi-1.14.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2"},
{file = "cffi-1.14.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123"},
{file = "cffi-1.14.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1"},
{file = "cffi-1.14.2-cp36-cp36m-win32.whl", hash = "sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281"},
{file = "cffi-1.14.2-cp36-cp36m-win_amd64.whl", hash = "sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4"},
{file = "cffi-1.14.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798"},
{file = "cffi-1.14.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4"},
{file = "cffi-1.14.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f"},
{file = "cffi-1.14.2-cp37-cp37m-win32.whl", hash = "sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650"},
{file = "cffi-1.14.2-cp37-cp37m-win_amd64.whl", hash = "sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15"},
{file = "cffi-1.14.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa"},
{file = "cffi-1.14.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c"},
{file = "cffi-1.14.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75"},
{file = "cffi-1.14.2-cp38-cp38-win32.whl", hash = "sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e"},
{file = "cffi-1.14.2-cp38-cp38-win_amd64.whl", hash = "sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c"},
{file = "cffi-1.14.2.tar.gz", hash = "sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b"},
]
click = [
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
@@ -1017,11 +1087,38 @@ colorama = [
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
]
cvxopt = [
{file = "cvxopt-1.2.5-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:78d0ea93f71f4b5b882ecd741ddc1e0cc8b06f4a2881f9d3fa55631d633273b6"},
{file = "cvxopt-1.2.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:fcc447eb3a0465f9d6b78f3df9861cb6fdb18ff7ad008e17f614da580e1fdbee"},
{file = "cvxopt-1.2.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:d74806ea13bbe5829903d048e19402051e784e4b34a86cdee2ba80d13f29a54f"},
{file = "cvxopt-1.2.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:50c464cbb43d1e350759294107abbef29576ea70605f5ac86db7c022002ab22f"},
{file = "cvxopt-1.2.5-cp27-cp27m-win_amd64.whl", hash = "sha256:60daaf297d195b33eab6a90c6fd9e8939541fdb209ed2972b70fba6dcbf82192"},
{file = "cvxopt-1.2.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ae3684ffabb135a059ba2c1947c0c1e00d79374967f40e807cb0fce65e514999"},
{file = "cvxopt-1.2.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6517bde92411c6b57d09a60c51ef066952400b0b21cf26694a0b0b0fc3faa065"},
{file = "cvxopt-1.2.5-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:01b579ef42258f129fc1f249d5bd2945b6183e4323ff9a45a61d1224d76ab778"},
{file = "cvxopt-1.2.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:4dcb001b1c6a40ec9c61103816aae0d32dbc11894e0f5f645668f524a051bcaa"},
{file = "cvxopt-1.2.5-cp35-cp35m-win_amd64.whl", hash = "sha256:8095c783278b3b3084b0ea431fdec072f5c17ba7b4249b3a71b3866f00fd0d9f"},
{file = "cvxopt-1.2.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7737e406e9d7ecfd86d93492c619238969023ea2b4556bf576b4a7be149aa9f0"},
{file = "cvxopt-1.2.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:9ef3bade9effc568b4b077166d5dde9632c2ead8d71e3f6c930e08ae29c7bb64"},
{file = "cvxopt-1.2.5-cp36-cp36m-win_amd64.whl", hash = "sha256:02976910f5ff456246e0454d32babe61aa5c7e4c781a330840347fcf3c54f5fa"},
{file = "cvxopt-1.2.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1d56655abfd7e0d3051d71bffe322ce77d71e5d683bcc6a359fe207dbff02a8b"},
{file = "cvxopt-1.2.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8bcb1e4b6a92ecbd6b1bea630b666d12cf253c2b902e5d039653e850defff887"},
{file = "cvxopt-1.2.5-cp37-cp37m-win_amd64.whl", hash = "sha256:dd2c97ace2739f31bceadd29e0e9535fe7457d756a6d9d207e1a654fd1ad1dc2"},
{file = "cvxopt-1.2.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37768ccefd3aec0dd9428d5eaa1a075d2db1d485e9b93c4d5d02464297a6530a"},
{file = "cvxopt-1.2.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3446b360c4123949511b4aef0b3d0bcdd3ce485b8e682c221a39c886945eacd3"},
{file = "cvxopt-1.2.5.tar.gz", hash = "sha256:94ec8c36bd6628a11de9014346692daeeef99b3b7bae28cef30c7490bbcb2d72"},
]
cvxpy = [
{file = "cvxpy-1.0.31-cp27-cp27m-macosx_10_7_x86_64.whl", hash = "sha256:e88982b2817252803c45c3c6bd4f8919c0684c4b6595da953249253e612e3729"},
{file = "cvxpy-1.0.31-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:513fe05931a0c93e25d4b4ccca84d7145a43b014d7a29a981d54c76a527bc0bf"},
{file = "cvxpy-1.0.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4db9ec8593e8ef31cc8a22f5eb09e36460877724dcd9dc807b26e6768522e7a2"},
{file = "cvxpy-1.0.31.tar.gz", hash = "sha256:b398754f9e9ceaa46b07806b5ae85f90fd8de748475db22a6b99c5943cebe69d"},
{file = "cvxpy-1.1.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a21e6a5c49ff04b29c47ecd1c9d4e3dee070c1d1df4f71a310addc2e54f85c7e"},
{file = "cvxpy-1.1.5-cp35-cp35m-win_amd64.whl", hash = "sha256:20f3ae7b732b33475a44ec2c810fefc0717fed35e3c677611040049751f3c854"},
{file = "cvxpy-1.1.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9d4b7fad457875b90ba7ca93ca9797715dcfe8bc2044a13eed5ce3735ca4bdf6"},
{file = "cvxpy-1.1.5-cp36-cp36m-win_amd64.whl", hash = "sha256:d12ce601b97264295f5c59d55a7fe1f479c074cbb064e7aa8818442c04c84a9d"},
{file = "cvxpy-1.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:51280efcc041855bb009bf304d0005d5b352e8027f09d40d60f69cc056e2afa2"},
{file = "cvxpy-1.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:7c10f6402fc1f075be7ac0ca0ad31340bac5a6ad4bbc01f94fa2232ec6f784d4"},
{file = "cvxpy-1.1.5-py3.5-win-amd64.egg", hash = "sha256:51e61497a1aaa43ee78430929e77f8acaf1bc9d2cb4187fee3a9a4f7f790230d"},
{file = "cvxpy-1.1.5-py3.6-win-amd64.egg", hash = "sha256:683fd7f56457a7f2375a47e2ed952adcdf47fffd290e56d213fee73ff115b315"},
{file = "cvxpy-1.1.5-py3.7-win-amd64.egg", hash = "sha256:382ea3dca38b4cf0004c1b9fa0fd2c97d07f2c1fbfd5da667e45a44ff622a297"},
{file = "cvxpy-1.1.5.tar.gz", hash = "sha256:7c826a874db2e4cefe54e63ebd3a3763d0d72e55a17c7d1cfec80008a87b8d81"},
]
decorator = [
{file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"},
@@ -1031,9 +1128,6 @@ defusedxml = [
{file = "defusedxml-0.6.0-py2.py3-none-any.whl", hash = "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93"},
{file = "defusedxml-0.6.0.tar.gz", hash = "sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5"},
]
dill = [
{file = "dill-0.3.1.1.tar.gz", hash = "sha256:42d8ef819367516592a825746a18073ced42ca169ab1f5f4044134703e7a049c"},
]
ecos = [
{file = "ecos-2.0.7.post1-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:dd9f01e28fe58894fb394931804884122606fb4e2a59d4514b803e9cd11b7d2b"},
{file = "ecos-2.0.7.post1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:db7433051f6072d4821ebc582e9ff853d7d631ed98770550d248eae70b29dd26"},
@@ -1063,19 +1157,19 @@ entrypoints = [
{file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"},
]
flake8 = [
{file = "flake8-3.8.2-py2.py3-none-any.whl", hash = "sha256:ccaa799ef9893cebe69fdfefed76865aeaefbb94cb8545617b2298786a4de9a5"},
{file = "flake8-3.8.2.tar.gz", hash = "sha256:c69ac1668e434d37a2d2880b3ca9aafd54b3a10a3ac1ab101d22f29e29cf8634"},
{file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"},
{file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"},
]
future = [
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
]
importlib-metadata = [
{file = "importlib_metadata-1.6.1-py2.py3-none-any.whl", hash = "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958"},
{file = "importlib_metadata-1.6.1.tar.gz", hash = "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545"},
{file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"},
{file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"},
]
ipykernel = [
{file = "ipykernel-5.3.0-py3-none-any.whl", hash = "sha256:a8362e3ae365023ca458effe93b026b8cdadc0b73ff3031472128dd8a2cf0289"},
{file = "ipykernel-5.3.0.tar.gz", hash = "sha256:731adb3f2c4ebcaff52e10a855ddc87670359a89c9c784d711e62d66fccdafae"},
{file = "ipykernel-5.3.4-py3-none-any.whl", hash = "sha256:d6fbba26dba3cebd411382bc484f7bc2caa98427ae0ddb4ab37fe8bfeb5c7dd3"},
{file = "ipykernel-5.3.4.tar.gz", hash = "sha256:9b2652af1607986a1b231c62302d070bc0534f564c393a5d9d130db9abbbe89d"},
]
ipython = [
{file = "ipython-7.9.0-py3-none-any.whl", hash = "sha256:ed7ebe1cba899c1c3ccad6f7f1c2d2369464cc77dba8eebc65e2043e19cda995"},
@@ -1090,8 +1184,8 @@ ipywidgets = [
{file = "ipywidgets-7.5.1.tar.gz", hash = "sha256:e945f6e02854a74994c596d9db83444a1850c01648f1574adf144fbbabe05c97"},
]
jedi = [
{file = "jedi-0.17.0-py2.py3-none-any.whl", hash = "sha256:cd60c93b71944d628ccac47df9a60fec53150de53d42dc10a7fc4b5ba6aae798"},
{file = "jedi-0.17.0.tar.gz", hash = "sha256:df40c97641cb943661d2db4c33c2e1ff75d491189423249e989bcea4464f3030"},
{file = "jedi-0.17.2-py2.py3-none-any.whl", hash = "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5"},
{file = "jedi-0.17.2.tar.gz", hash = "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20"},
]
jinja2 = [
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
@@ -1107,8 +1201,8 @@ jupyter = [
{file = "jupyter-1.0.0.zip", hash = "sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7"},
]
jupyter-client = [
{file = "jupyter_client-6.1.3-py3-none-any.whl", hash = "sha256:cde8e83aab3ec1c614f221ae54713a9a46d3bf28292609d2db1b439bef5a8c8e"},
{file = "jupyter_client-6.1.3.tar.gz", hash = "sha256:3a32fa4d0b16d1c626b30c3002a62dfd86d6863ed39eaba3f537fade197bb756"},
{file = "jupyter_client-6.1.7-py3-none-any.whl", hash = "sha256:c958d24d6eacb975c1acebb68ac9077da61b5f5c040f22f6849928ad7393b950"},
{file = "jupyter_client-6.1.7.tar.gz", hash = "sha256:49e390b36fe4b4226724704ea28d9fb903f1a3601b6882ce3105221cd09377a1"},
]
jupyter-console = [
{file = "jupyter_console-6.1.0-py2.py3-none-any.whl", hash = "sha256:b392155112ec86a329df03b225749a0fa903aa80811e8eda55796a40b5e470d8"},
@@ -1157,25 +1251,20 @@ mistune = [
{file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"},
]
more-itertools = [
{file = "more-itertools-8.3.0.tar.gz", hash = "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be"},
{file = "more_itertools-8.3.0-py3-none-any.whl", hash = "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982"},
]
multiprocess = [
{file = "multiprocess-0.70.9-cp27-cp27m-win32.whl", hash = "sha256:0e4e65c2e74aa14fa0c9a1f838b5e9a5f8fe5b3a173925792260843c4a6157ec"},
{file = "multiprocess-0.70.9-cp27-cp27m-win_amd64.whl", hash = "sha256:1eb7dfe2d809d53be92e8a288ed1c01614fe5407bbc9d078ed451a749fb1bd34"},
{file = "multiprocess-0.70.9.tar.gz", hash = "sha256:9fd5bd990132da77e73dec6e9613408602a4612e1d73caf2e2b813d2b61508e5"},
{file = "more-itertools-8.5.0.tar.gz", hash = "sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20"},
{file = "more_itertools-8.5.0-py3-none-any.whl", hash = "sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c"},
]
nbconvert = [
{file = "nbconvert-5.6.1-py2.py3-none-any.whl", hash = "sha256:f0d6ec03875f96df45aa13e21fd9b8450c42d7e1830418cccc008c0df725fcee"},
{file = "nbconvert-5.6.1.tar.gz", hash = "sha256:21fb48e700b43e82ba0e3142421a659d7739b65568cc832a13976a77be16b523"},
]
nbformat = [
{file = "nbformat-5.0.6-py3-none-any.whl", hash = "sha256:276343c78a9660ab2a63c28cc33da5f7c58c092b3f3a40b6017ae2ce6689320d"},
{file = "nbformat-5.0.6.tar.gz", hash = "sha256:049af048ed76b95c3c44043620c17e56bc001329e07f83fec4f177f0e3d7b757"},
{file = "nbformat-5.0.7-py3-none-any.whl", hash = "sha256:ea55c9b817855e2dfcd3f66d74857342612a60b1f09653440f4a5845e6e3523f"},
{file = "nbformat-5.0.7.tar.gz", hash = "sha256:54d4d6354835a936bad7e8182dcd003ca3dc0cedfee5a306090e04854343b340"},
]
notebook = [
{file = "notebook-6.0.3-py3-none-any.whl", hash = "sha256:3edc616c684214292994a3af05eaea4cc043f6b4247d830f3a2f209fa7639a80"},
{file = "notebook-6.0.3.tar.gz", hash = "sha256:47a9092975c9e7965ada00b9a20f0cf637d001db60d241d479f53c0be117ad48"},
{file = "notebook-6.1.3-py3-none-any.whl", hash = "sha256:964cc40cff68e473f3778aef9266e867f7703cb4aebdfd250f334efe02f64c86"},
{file = "notebook-6.1.3.tar.gz", hash = "sha256:9990d51b9931a31e681635899aeb198b4c4b41586a9e87fbfaaed1a71d0a05b6"},
]
numpy = [
{file = "numpy-1.18.5-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:e91d31b34fc7c2c8f756b4e902f901f856ae53a93399368d9a0dc7be17ed2ca0"},
@@ -1268,8 +1357,8 @@ pandocfilters = [
{file = "pandocfilters-1.4.2.tar.gz", hash = "sha256:b3dd70e169bb5449e6bc6ff96aea89c5eea8c5f6ab5e207fc2f521a2cf4a0da9"},
]
parso = [
{file = "parso-0.7.0-py2.py3-none-any.whl", hash = "sha256:158c140fc04112dc45bca311633ae5033c2c2a7b732fa33d0955bad8152a8dd0"},
{file = "parso-0.7.0.tar.gz", hash = "sha256:908e9fae2144a076d72ae4e25539143d40b8e3eafbaeae03c1bfe226f4cdf12c"},
{file = "parso-0.7.1-py2.py3-none-any.whl", hash = "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea"},
{file = "parso-0.7.1.tar.gz", hash = "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9"},
]
pathlib2 = [
{file = "pathlib2-2.3.5-py2.py3-none-any.whl", hash = "sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db"},
@@ -1301,13 +1390,17 @@ ptyprocess = [
{file = "ptyprocess-0.6.0.tar.gz", hash = "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0"},
]
py = [
{file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"},
{file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"},
{file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
{file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
]
pycodestyle = [
{file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},
{file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"},
]
pycparser = [
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
]
pyflakes = [
{file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"},
{file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
@@ -1336,18 +1429,18 @@ pytz = [
{file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"},
]
pywin32 = [
{file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"},
{file = "pywin32-227-cp27-cp27m-win_amd64.whl", hash = "sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116"},
{file = "pywin32-227-cp35-cp35m-win32.whl", hash = "sha256:f4c5be1a293bae0076d93c88f37ee8da68136744588bc5e2be2f299a34ceb7aa"},
{file = "pywin32-227-cp35-cp35m-win_amd64.whl", hash = "sha256:a929a4af626e530383a579431b70e512e736e9588106715215bf685a3ea508d4"},
{file = "pywin32-227-cp36-cp36m-win32.whl", hash = "sha256:300a2db938e98c3e7e2093e4491439e62287d0d493fe07cce110db070b54c0be"},
{file = "pywin32-227-cp36-cp36m-win_amd64.whl", hash = "sha256:9b31e009564fb95db160f154e2aa195ed66bcc4c058ed72850d047141b36f3a2"},
{file = "pywin32-227-cp37-cp37m-win32.whl", hash = "sha256:47a3c7551376a865dd8d095a98deba954a98f326c6fe3c72d8726ca6e6b15507"},
{file = "pywin32-227-cp37-cp37m-win_amd64.whl", hash = "sha256:31f88a89139cb2adc40f8f0e65ee56a8c585f629974f9e07622ba80199057511"},
{file = "pywin32-227-cp38-cp38-win32.whl", hash = "sha256:7f18199fbf29ca99dff10e1f09451582ae9e372a892ff03a28528a24d55875bc"},
{file = "pywin32-227-cp38-cp38-win_amd64.whl", hash = "sha256:7c1ae32c489dc012930787f06244426f8356e129184a02c25aef163917ce158e"},
{file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"},
{file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"},
{file = "pywin32-228-cp27-cp27m-win32.whl", hash = "sha256:37dc9935f6a383cc744315ae0c2882ba1768d9b06700a70f35dc1ce73cd4ba9c"},
{file = "pywin32-228-cp27-cp27m-win_amd64.whl", hash = "sha256:11cb6610efc2f078c9e6d8f5d0f957620c333f4b23466931a247fb945ed35e89"},
{file = "pywin32-228-cp35-cp35m-win32.whl", hash = "sha256:1f45db18af5d36195447b2cffacd182fe2d296849ba0aecdab24d3852fbf3f80"},
{file = "pywin32-228-cp35-cp35m-win_amd64.whl", hash = "sha256:6e38c44097a834a4707c1b63efa9c2435f5a42afabff634a17f563bc478dfcc8"},
{file = "pywin32-228-cp36-cp36m-win32.whl", hash = "sha256:ec16d44b49b5f34e99eb97cf270806fdc560dff6f84d281eb2fcb89a014a56a9"},
{file = "pywin32-228-cp36-cp36m-win_amd64.whl", hash = "sha256:a60d795c6590a5b6baeacd16c583d91cce8038f959bd80c53bd9a68f40130f2d"},
{file = "pywin32-228-cp37-cp37m-win32.whl", hash = "sha256:af40887b6fc200eafe4d7742c48417529a8702dcc1a60bf89eee152d1d11209f"},
{file = "pywin32-228-cp37-cp37m-win_amd64.whl", hash = "sha256:00eaf43dbd05ba6a9b0080c77e161e0b7a601f9a3f660727a952e40140537de7"},
{file = "pywin32-228-cp38-cp38-win32.whl", hash = "sha256:fa6ba028909cfc64ce9e24bcf22f588b14871980d9787f1e2002c99af8f1850c"},
{file = "pywin32-228-cp38-cp38-win_amd64.whl", hash = "sha256:9b3466083f8271e1a5eb0329f4e0d61925d46b40b195a33413e0905dccb285e8"},
{file = "pywin32-228-cp39-cp39-win32.whl", hash = "sha256:ed74b72d8059a6606f64842e7917aeee99159ebd6b8d6261c518d002837be298"},
{file = "pywin32-228-cp39-cp39-win_amd64.whl", hash = "sha256:8319bafdcd90b7202c50d6014efdfe4fde9311b3ff15fd6f893a45c0868de203"},
]
pywinpty = [
{file = "pywinpty-0.5.7-cp27-cp27m-win32.whl", hash = "sha256:b358cb552c0f6baf790de375fab96524a0498c9df83489b8c23f7f08795e966b"},
@@ -1362,38 +1455,38 @@ pywinpty = [
{file = "pywinpty-0.5.7.tar.gz", hash = "sha256:2d7e9c881638a72ffdca3f5417dd1563b60f603e1b43e5895674c2a1b01f95a0"},
]
pyzmq = [
{file = "pyzmq-19.0.1-cp27-cp27m-macosx_10_9_intel.whl", hash = "sha256:58688a2dfa044fad608a8e70ba8d019d0b872ec2acd75b7b5e37da8905605891"},
{file = "pyzmq-19.0.1-cp27-cp27m-win32.whl", hash = "sha256:87c78f6936e2654397ca2979c1d323ee4a889eef536cc77a938c6b5be33351a7"},
{file = "pyzmq-19.0.1-cp27-cp27m-win_amd64.whl", hash = "sha256:97b6255ae77328d0e80593681826a0479cb7bac0ba8251b4dd882f5145a2293a"},
{file = "pyzmq-19.0.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:15b4cb21118f4589c4db8be4ac12b21c8b4d0d42b3ee435d47f686c32fe2e91f"},
{file = "pyzmq-19.0.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:931339ac2000d12fe212e64f98ce291e81a7ec6c73b125f17cf08415b753c087"},
{file = "pyzmq-19.0.1-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:2a88b8fabd9cc35bd59194a7723f3122166811ece8b74018147a4ed8489e6421"},
{file = "pyzmq-19.0.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:bafd651b557dd81d89bd5f9c678872f3e7b7255c1c751b78d520df2caac80230"},
{file = "pyzmq-19.0.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8952f6ba6ae598e792703f3134af5a01af8f5c7cf07e9a148f05a12b02412cea"},
{file = "pyzmq-19.0.1-cp35-cp35m-win32.whl", hash = "sha256:54aa24fd60c4262286fc64ca632f9e747c7cc3a3a1144827490e1dc9b8a3a960"},
{file = "pyzmq-19.0.1-cp35-cp35m-win_amd64.whl", hash = "sha256:dcbc3f30c11c60d709c30a213dc56e88ac016fe76ac6768e64717bd976072566"},
{file = "pyzmq-19.0.1-cp36-cp36m-macosx_10_9_intel.whl", hash = "sha256:6ca519309703e95d55965735a667809bbb65f52beda2fdb6312385d3e7a6d234"},
{file = "pyzmq-19.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4ee0bfd82077a3ff11c985369529b12853a4064320523f8e5079b630f9551448"},
{file = "pyzmq-19.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ba6f24431b569aec674ede49cad197cad59571c12deed6ad8e3c596da8288217"},
{file = "pyzmq-19.0.1-cp36-cp36m-win32.whl", hash = "sha256:956775444d01331c7eb412c5fb9bb62130dfaac77e09f32764ea1865234e2ca9"},
{file = "pyzmq-19.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b08780e3a55215873b3b8e6e7ca8987f14c902a24b6ac081b344fd430d6ca7cd"},
{file = "pyzmq-19.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:21f7d91f3536f480cb2c10d0756bfa717927090b7fb863e6323f766e5461ee1c"},
{file = "pyzmq-19.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:bfff5ffff051f5aa47ba3b379d87bd051c3196b0c8a603e8b7ed68a6b4f217ec"},
{file = "pyzmq-19.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:07fb8fe6826a229dada876956590135871de60dbc7de5a18c3bcce2ed1f03c98"},
{file = "pyzmq-19.0.1-cp37-cp37m-win32.whl", hash = "sha256:342fb8a1dddc569bc361387782e8088071593e7eaf3e3ecf7d6bd4976edff112"},
{file = "pyzmq-19.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:faee2604f279d31312bc455f3d024f160b6168b9c1dde22bf62d8c88a4deca8e"},
{file = "pyzmq-19.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b9d21fc56c8aacd2e6d14738021a9d64f3f69b30578a99325a728e38a349f85"},
{file = "pyzmq-19.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:af0c02cf49f4f9eedf38edb4f3b6bb621d83026e7e5d76eb5526cc5333782fd6"},
{file = "pyzmq-19.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5f1f2eb22aab606f808163eb1d537ac9a0ba4283fbeb7a62eb48d9103cf015c2"},
{file = "pyzmq-19.0.1-cp38-cp38-win32.whl", hash = "sha256:f9d7e742fb0196992477415bb34366c12e9bb9a0699b8b3f221ff93b213d7bec"},
{file = "pyzmq-19.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:5b99c2ae8089ef50223c28bac57510c163bfdff158c9e90764f812b94e69a0e6"},
{file = "pyzmq-19.0.1-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:cf5d689ba9513b9753959164cf500079383bc18859f58bf8ce06d8d4bef2b054"},
{file = "pyzmq-19.0.1-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:aaa8b40b676576fd7806839a5de8e6d5d1b74981e6376d862af6c117af2a3c10"},
{file = "pyzmq-19.0.1.tar.gz", hash = "sha256:13a5638ab24d628a6ade8f794195e1a1acd573496c3b85af2f1183603b7bf5e0"},
{file = "pyzmq-19.0.2-cp27-cp27m-macosx_10_9_intel.whl", hash = "sha256:59f1e54627483dcf61c663941d94c4af9bf4163aec334171686cdaee67974fe5"},
{file = "pyzmq-19.0.2-cp27-cp27m-win32.whl", hash = "sha256:c36ffe1e5aa35a1af6a96640d723d0d211c5f48841735c2aa8d034204e87eb87"},
{file = "pyzmq-19.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:0a422fc290d03958899743db091f8154958410fc76ce7ee0ceb66150f72c2c97"},
{file = "pyzmq-19.0.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c20dd60b9428f532bc59f2ef6d3b1029a28fc790d408af82f871a7db03e722ff"},
{file = "pyzmq-19.0.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d46fb17f5693244de83e434648b3dbb4f4b0fec88415d6cbab1c1452b6f2ae17"},
{file = "pyzmq-19.0.2-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:f1a25a61495b6f7bb986accc5b597a3541d9bd3ef0016f50be16dbb32025b302"},
{file = "pyzmq-19.0.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:ab0d01148d13854de716786ca73701012e07dff4dfbbd68c4e06d8888743526e"},
{file = "pyzmq-19.0.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:720d2b6083498a9281eaee3f2927486e9fe02cd16d13a844f2e95217f243efea"},
{file = "pyzmq-19.0.2-cp35-cp35m-win32.whl", hash = "sha256:29d51279060d0a70f551663bc592418bcad7f4be4eea7b324f6dd81de05cb4c1"},
{file = "pyzmq-19.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:5120c64646e75f6db20cc16b9a94203926ead5d633de9feba4f137004241221d"},
{file = "pyzmq-19.0.2-cp36-cp36m-macosx_10_9_intel.whl", hash = "sha256:8a6ada5a3f719bf46a04ba38595073df8d6b067316c011180102ba2a1925f5b5"},
{file = "pyzmq-19.0.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fa411b1d8f371d3a49d31b0789eb6da2537dadbb2aef74a43aa99a78195c3f76"},
{file = "pyzmq-19.0.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:00dca814469436455399660247d74045172955459c0bd49b54a540ce4d652185"},
{file = "pyzmq-19.0.2-cp36-cp36m-win32.whl", hash = "sha256:046b92e860914e39612e84fa760fc3f16054d268c11e0e25dcb011fb1bc6a075"},
{file = "pyzmq-19.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99cc0e339a731c6a34109e5c4072aaa06d8e32c0b93dc2c2d90345dd45fa196c"},
{file = "pyzmq-19.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e36f12f503511d72d9bdfae11cadbadca22ff632ff67c1b5459f69756a029c19"},
{file = "pyzmq-19.0.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c40fbb2b9933369e994b837ee72193d6a4c35dfb9a7c573257ef7ff28961272c"},
{file = "pyzmq-19.0.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5d9fc809aa8d636e757e4ced2302569d6e60e9b9c26114a83f0d9d6519c40493"},
{file = "pyzmq-19.0.2-cp37-cp37m-win32.whl", hash = "sha256:3fa6debf4bf9412e59353defad1f8035a1e68b66095a94ead8f7a61ae90b2675"},
{file = "pyzmq-19.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:73483a2caaa0264ac717af33d6fb3f143d8379e60a422730ee8d010526ce1913"},
{file = "pyzmq-19.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:36ab114021c0cab1a423fe6689355e8f813979f2c750968833b318c1fa10a0fd"},
{file = "pyzmq-19.0.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8b66b94fe6243d2d1d89bca336b2424399aac57932858b9a30309803ffc28112"},
{file = "pyzmq-19.0.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:654d3e06a4edc566b416c10293064732516cf8871a4522e0a2ba00cc2a2e600c"},
{file = "pyzmq-19.0.2-cp38-cp38-win32.whl", hash = "sha256:276ad604bffd70992a386a84bea34883e696a6b22e7378053e5d3227321d9702"},
{file = "pyzmq-19.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:09d24a80ccb8cbda1af6ed8eb26b005b6743e58e9290566d2a6841f4e31fa8e0"},
{file = "pyzmq-19.0.2-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:c1a31cd42905b405530e92bdb70a8a56f048c8a371728b8acf9d746ecd4482c0"},
{file = "pyzmq-19.0.2-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a7e7f930039ee0c4c26e4dfee015f20bd6919cd8b97c9cd7afbde2923a5167b6"},
{file = "pyzmq-19.0.2.tar.gz", hash = "sha256:296540a065c8c21b26d63e3cea2d1d57902373b16e4256afe46422691903a438"},
]
qtconsole = [
{file = "qtconsole-4.7.4-py2.py3-none-any.whl", hash = "sha256:89442727940126c65c2f94a058f1b4693a0f5d4c4b192fd6518ba3b11f4791aa"},
{file = "qtconsole-4.7.4.tar.gz", hash = "sha256:fd48bf1051d6e69cec1f9e2596cfaa94e3c726c70c5d848681ebce10c029f5fd"},
{file = "qtconsole-4.7.6-py2.py3-none-any.whl", hash = "sha256:570b9e1dd4f9b727699b0ed04c6943d9d32d5a2085aa69d82d814e039bbcf74b"},
{file = "qtconsole-4.7.6.tar.gz", hash = "sha256:6c24397c19a49a5cf69582c931db4b0f6b00a78530a2bfd122936f2ebfae2fef"},
]
qtpy = [
{file = "QtPy-1.9.0-py2.py3-none-any.whl", hash = "sha256:fa0b8363b363e89b2a6f49eddc162a04c0699ae95e109a6be3bb145a913190ea"},
@@ -1466,8 +1559,8 @@ traitlets = [
{file = "traitlets-4.3.3.tar.gz", hash = "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"},
]
wcwidth = [
{file = "wcwidth-0.2.3-py2.py3-none-any.whl", hash = "sha256:980fbf4f3c196c0f329cdcd1e84c554d6a211f18e252e525a0cf4223154a41d6"},
{file = "wcwidth-0.2.3.tar.gz", hash = "sha256:edbc2b718b4db6cdf393eefe3a420183947d6aa312505ce6754516f458ff8830"},
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
]
webencodings = [
{file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},

View File

@@ -220,8 +220,8 @@ class BaseConvexOptimizer(BaseOptimizer):
opt.solve(solver=self._solver, verbose=self._verbose)
else:
opt.solve(verbose=self._verbose)
except (TypeError, cp.DCPError):
raise exceptions.OptimizationError
except (TypeError, cp.DCPError) as e:
raise exceptions.OptimizationError from e
if opt.status != "optimal":
raise exceptions.OptimizationError
@@ -341,6 +341,7 @@ class BaseConvexOptimizer(BaseOptimizer):
weights_sum_to_one=True,
constraints=None,
solver="SLSQP",
initial_guess=None,
):
"""
Optimise some objective function using the scipy backend. This can
@@ -374,6 +375,8 @@ class BaseConvexOptimizer(BaseOptimizer):
:param solver: which SCIPY solver to use, e.g "SLSQP", "COBYLA", "BFGS".
User beware: different optimisers 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 optimise the custom objective
:rtype: OrderedDict
"""
@@ -385,7 +388,8 @@ class BaseConvexOptimizer(BaseOptimizer):
bound_array = np.vstack((self._lower_bounds, self._upper_bounds)).T
bounds = list(map(tuple, bound_array))
initial_guess = np.array([1 / self.n_assets] * self.n_assets)
if initial_guess is None:
initial_guess = np.array([1 / self.n_assets] * self.n_assets)
# Construct constraints
final_constraints = []

View File

@@ -375,7 +375,7 @@ class CLA(base_optimizer.BaseOptimizer):
"""
Maximise the Sharpe ratio.
:return: asset weights for the volatility-minimising portfolio
:return: asset weights for the max-sharpe portfolio
:rtype: OrderedDict
"""
if not self.w:

View File

@@ -297,19 +297,21 @@ class DiscreteAllocation:
# Integer allocation
x = cp.Variable(n, integer=True)
# Remaining dollars
r = self.total_portfolio_value - p.T * x
r = self.total_portfolio_value - p.T @ x
# Objective function is remaining dollars + sum of absolute deviations from ideality.
objective = r + cp.norm(w * self.total_portfolio_value - cp.multiply(x, p), 1)
constraints = [r + p.T * x == self.total_portfolio_value, x >= 0, r >= 0]
# Set up linear program
eta = w * self.total_portfolio_value - cp.multiply(x, p)
u = cp.Variable(n)
constraints = [eta <= u, eta >= -u, x >= 0, r >= 0]
objective = cp.sum(u) + r
opt = cp.Problem(cp.Minimize(objective), constraints)
opt.solve()
opt.solve(solver="GLPK_MI")
if opt.status not in {"optimal", "optimal_inaccurate"}:
raise exceptions.OptimizationError("Please try greedy_portfolio")
vals = np.rint(x.value)
vals = np.rint(x.value).astype(int)
self.allocation = self._remove_zero_positions(
collections.OrderedDict(zip([i[0] for i in self.weights], vals))
)

View File

@@ -271,6 +271,15 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
if not isinstance(target_volatility, (float, int)) or target_volatility < 0:
raise ValueError("target_volatility should be a positive float")
global_min_volatility = np.sqrt(1 / np.sum(np.linalg.inv(self.cov_matrix)))
if target_volatility < global_min_volatility:
raise ValueError(
"The minimum volatility is {:.3f}. Please use a higher target_volatility".format(
global_min_volatility
)
)
self._objective = objective_functions.portfolio_return(
self._w, self.expected_returns
)

View File

@@ -3,7 +3,6 @@ The ``exceptions`` module houses custom exceptions. Currently implemented:
- OptimizationError
"""
import traceback
class OptimizationError(Exception):
@@ -16,9 +15,4 @@ class OptimizationError(Exception):
default_message = (
"Please check your objectives/constraints or use a different solver."
)
if not (args or kwargs):
args = (default_message,)
super().__init__(*args, **kwargs)
traceback.print_exc()
super().__init__(default_message, *args, **kwargs)

View File

@@ -15,7 +15,6 @@ Currently implemented:
- general return model function, allowing you to run any return model from one function.
- mean historical return
- exponentially weighted mean historical return
- James-Stein shrinkage
- CAPM estimate of returns
Additionally, we provide utility functions to convert from returns to prices and vice-versa.
@@ -26,17 +25,22 @@ import pandas as pd
import numpy as np
def returns_from_prices(prices):
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
"""
return prices.pct_change().dropna(how="all")
if log_returns:
return np.log(1 + prices.pct_change()).dropna(how="all")
else:
return prices.pct_change().dropna(how="all")
def log_returns_from_prices(prices):
@@ -49,10 +53,13 @@ def log_returns_from_prices(prices):
:return: (daily) returns
:rtype: pd.DataFrame
"""
warnings.warn(
"log_returns_from_prices is deprecated. Please use returns_from_prices(prices, log_returns=True)"
)
return np.log(1 + prices.pct_change()).dropna(how="all")
def prices_from_returns(returns):
def prices_from_returns(returns, log_returns=False):
"""
Calculate the pseudo-prices given returns. These are not true prices because
the initial prices are all set to 1, but it behaves as intended when passed
@@ -60,9 +67,13 @@ def prices_from_returns(returns):
: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
"""
if log_returns:
returns = np.exp(returns)
ret = 1 + returns
ret.iloc[0] = 1 # set first day pseudo-price
return ret.cumprod()
@@ -81,7 +92,6 @@ def return_model(prices, method="mean_historical_return", **kwargs):
- ``mean_historical_return``
- ``ema_historical_return``
- ``james_stein_shrinkage``
- ``capm_return``
:type method: str, optional
@@ -93,19 +103,16 @@ def return_model(prices, method="mean_historical_return", **kwargs):
return mean_historical_return(prices, **kwargs)
elif method == "ema_historical_return":
return ema_historical_return(prices, **kwargs)
elif method == "james_stein_shrinkage":
return james_stein_shrinkage(prices, **kwargs)
elif method == "capm_return":
return capm_return(prices, **kwargs)
else:
raise NotImplementedError("Return model {} not implemented".format(method))
def mean_historical_return(
prices, returns_data=False, compounding=False, frequency=252
):
def mean_historical_return(prices, returns_data=False, compounding=True, frequency=252):
"""
Calculate annualised mean (daily) historical return from input (daily) asset prices.
By default, this uses the arithmetic mean (correct if log_returns are used).
:param prices: adjusted closing prices of the asset, each row is a date
and each column is a ticker/id.
@@ -113,7 +120,7 @@ def mean_historical_return(
:param returns_data: if true, the first argument is returns instead of prices.
:type returns_data: bool, defaults to False.
:param compounding: whether to properly compound the returns, optional.
:type compounding: bool, defaults to False
: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
@@ -128,13 +135,13 @@ def mean_historical_return(
else:
returns = returns_from_prices(prices)
if compounding:
return (1 + returns.mean()) ** frequency - 1
return (1 + returns).prod() ** (frequency / returns.count()) - 1
else:
return returns.mean() * frequency
def ema_historical_return(
prices, returns_data=False, compounding=False, span=500, frequency=252
prices, returns_data=False, compounding=True, span=500, frequency=252
):
"""
Calculate the exponentially-weighted mean of (daily) historical returns, giving
@@ -146,7 +153,7 @@ def ema_historical_return(
:param returns_data: if true, the first argument is returns instead of prices.
:type returns_data: bool, defaults to False.
:param compounding: whether to properly compound the returns, optional.
:type compounding: bool, defaults to False
: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
@@ -169,49 +176,10 @@ def ema_historical_return(
return returns.ewm(span=span).mean().iloc[-1] * frequency
def james_stein_shrinkage(prices, returns_data=False, compounding=False, frequency=252):
r"""
Compute the James-Stein shrinkage estimator, i.e
.. math::
\hat{\mu}_i^{JS} = \hat{\kappa} \bar{\mu} + (1-\hat{\kappa}) \mu_i,
where :math:`\kappa` is the shrinkage parameter, :math:`\bar{\mu}` is the shrinkage
target (grand average), and :math:`\mu` is the vector of mean 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 compounded: whether to properly compound the returns, optional.
:type compounding: 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
:return: James-Stein estimate of annualised return
:rtype: pd.Series
"""
if not isinstance(prices, pd.DataFrame):
warnings.warn("prices are not in a dataframe", RuntimeWarning)
prices = pd.DataFrame(prices)
if returns_data:
returns = prices
else:
returns = returns_from_prices(prices)
T, n = returns.shape
mu = returns.mean(axis=0)
mu_bar = mu.mean()
sigma_squared = 1 / T * mu_bar * (1 - mu_bar) # binomial estimate
kappa = 1 - (n - 3) * sigma_squared / np.sum((mu - mu_bar) ** 2)
theta_js = (1 - kappa) * mu + kappa * mu_bar
if compounding:
return (1 + theta_js) ** frequency - 1
else:
return theta_js * frequency
def james_stein_shrinkage(prices, returns_data=False, compounding=True, frequency=252):
raise NotImplementedError(
"Deprecated because its implementation here was misguided."
)
def capm_return(
@@ -219,7 +187,7 @@ def capm_return(
market_prices=None,
returns_data=False,
risk_free_rate=0.02,
compounding=False,
compounding=True,
frequency=252,
):
"""
@@ -244,7 +212,7 @@ def capm_return(
to the frequency parameter.
:type risk_free_rate: float, optional
:param compounding: whether to properly compound the returns, optional.
:type compounding: bool, defaults to False
: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
@@ -267,7 +235,6 @@ def capm_return(
if market_returns is None:
# Append market return to right and compute sample covariance matrix
returns["mkt"] = returns.mean(axis=1)
else:
market_returns.columns = ["mkt"]
returns = returns.join(market_returns, how="left")
@@ -279,7 +246,9 @@ def capm_return(
betas = betas.drop("mkt")
# Find mean market return on a given time period
if compounding:
mkt_mean_ret = (1 + returns["mkt"].mean()) ** frequency - 1
mkt_mean_ret = (1 + returns["mkt"]).prod() ** (
frequency / returns["mkt"].count()
) - 1
else:
mkt_mean_ret = returns["mkt"].mean() * frequency

View File

@@ -13,7 +13,6 @@ Currently implemented:
"""
import collections
import warnings
import numpy as np
import pandas as pd
import scipy.cluster.hierarchy as sch
@@ -139,13 +138,19 @@ class HRPOpt(base_optimizer.BaseOptimizer):
w[second_cluster] *= 1 - alpha # weight 2
return w
def optimize(self):
def optimize(self, linkage_method="single"):
"""
Construct a hierarchical risk parity portfolio
Construct a hierarchical risk parity portfolio, using Scipy hierarchical clustering
(see `here <https://docs.scipy.org/doc/scipy/reference/generated/scipy.cluster.hierarchy.linkage.html>`_)
:param linkage_method: which scipy linkage method to use
:type linkage_method: str
:return: weights for the HRP portfolio
:rtype: OrderedDict
"""
if linkage_method not in sch._LINKAGE_METHODS:
raise ValueError("linkage_method must be one recognised by scipy")
if self.returns is None:
cov = self.cov_matrix
corr = risk_models.cov_to_corr(self.cov_matrix).round(6)
@@ -159,7 +164,7 @@ class HRPOpt(base_optimizer.BaseOptimizer):
matrix = np.sqrt(np.clip((1.0 - corr) / 2.0, a_min=0.0, a_max=1.0))
dist = ssd.squareform(matrix, checks=False)
self.clusters = sch.linkage(dist, "single")
self.clusters = sch.linkage(dist, linkage_method)
sort_ix = HRPOpt._get_quasi_diag(self.clusters)
ordered_tickers = corr.index[sort_ix].tolist()
hrp = HRPOpt._raw_hrp_allocation(cov, ordered_tickers)

View File

@@ -117,6 +117,12 @@ def L2_reg(w, gamma=1):
r"""
L2 regularisation, i.e :math:`\gamma ||w||^2`, to increase the number of nonzero weights.
Example::
ef = EfficientFrontier(mu, S)
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

View File

@@ -39,7 +39,8 @@ def _is_positive_semidefinite(matrix):
:rtype: bool
"""
try:
np.linalg.cholesky(matrix)
# Significantly more efficient than checking eigenvalues (stackoverflow.com/questions/16266720)
np.linalg.cholesky(matrix + 1e-16 * np.eye(len(matrix)))
return True
except np.linalg.LinAlgError:
return False

View File

@@ -35,7 +35,8 @@ python = "^3.5"
numpy = "^1.12"
scipy = "^1.3"
pandas = ">=0.19"
cvxpy = "~1.0"
cvxpy = "^1.0"
cvxopt = "^1.2"
[tool.poetry.dev-dependencies]
pytest = "^4.6"

View File

@@ -3,4 +3,5 @@ numpy>=1.17
pandas>=0.19
scikit-learn>=0.19
scipy>=1.3
cvxpy==1.0.28
cvxpy>=1.0
cvxopt>=1.0

View File

@@ -143,26 +143,26 @@ def test_bl_returns_all_views():
posterior_rets,
np.array(
[
0.11774473,
0.1709139,
0.12180833,
0.21202423,
0.28120945,
-0.2787358,
0.17274774,
0.12714698,
0.25492005,
0.11229777,
0.07182723,
-0.01521839,
-0.21235465,
0.06399515,
-0.11738365,
0.28865661,
0.23828607,
0.12038049,
0.2331218,
0.10485376,
0.11168648,
0.16782938,
0.12516799,
0.24067997,
0.32848296,
-0.22789895,
0.16311297,
0.11928542,
0.25414308,
0.11007738,
0.06282615,
-0.03140218,
-0.16977172,
0.05254821,
-0.10463884,
0.32173375,
0.26399864,
0.1118594,
0.22999558,
0.08977448,
]
),
)

View File

@@ -28,7 +28,7 @@ def test_cla_max_sharpe_long_only():
np.testing.assert_allclose(
cla.portfolio_performance(),
(0.3253436663900292, 0.21333530089904357, 1.4312852355106793),
(0.2994470912768992, 0.21764331657015668, 1.283968171780824),
)
@@ -40,7 +40,7 @@ def test_cla_max_sharpe_short():
np.testing.assert_almost_equal(cla.weights.sum(), 1)
np.testing.assert_allclose(
cla.portfolio_performance(),
(0.3799273115521356, 0.23115368271125736, 1.5570909679242886),
(0.44859872371106785, 0.26762066559448255, 1.601515797589826),
)
sharpe = cla.portfolio_performance()[2]
@@ -72,7 +72,7 @@ def test_cla_min_volatility():
np.testing.assert_almost_equal(cla.weights.sum(), 1)
np.testing.assert_allclose(
cla.portfolio_performance(),
(0.1793123248125915, 0.15915084514118688, 1.00101463282373),
(0.1505682139948257, 0.15915084514118688, 0.8204054077060994),
)
@@ -99,7 +99,7 @@ def test_cla_max_sharpe_semicovariance():
np.testing.assert_almost_equal(cla.weights.sum(), 1)
np.testing.assert_allclose(
cla.portfolio_performance(),
(0.2936179968144084, 0.06362345488289835, 4.300583759841616),
(0.2686858719299194, 0.06489248187610204, 3.8322755539652578),
)
@@ -113,7 +113,7 @@ def test_cla_max_sharpe_exp_cov():
np.testing.assert_almost_equal(cla.weights.sum(), 1)
np.testing.assert_allclose(
cla.portfolio_performance(),
(0.3619453128519127, 0.1724297730592084, 1.9830990135009723),
(0.32971891062187103, 0.17670121760851704, 1.7527831149871063),
)
@@ -127,7 +127,7 @@ def test_cla_min_volatility_exp_cov_short():
np.testing.assert_almost_equal(cla.weights.sum(), 1)
np.testing.assert_allclose(
cla.portfolio_performance(),
(0.2634735528776959, 0.13259590618253303, 1.8362071642131053),
(0.23215576461823062, 0.1325959061825329, 1.6000174569958052),
)

View File

@@ -91,7 +91,7 @@ def test_custom_convex_logarithmic_barrier():
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.23978400459553223, 0.21100848889958182, 1.041588448605623),
(0.17261881638711316, 0.21100848889958182, 0.7232828270702603),
)
@@ -101,7 +101,7 @@ def test_custom_convex_deviation_risk_parity_error():
def deviation_risk_parity(w, cov_matrix):
n = cov_matrix.shape[0]
rp = (w * (cov_matrix @ w)) / cp.quad_form(w, cov_matrix)
rp = (w @ (cov_matrix @ w)) / cp.quad_form(w, cov_matrix)
return cp.sum_squares(rp - 1 / n)
with pytest.raises(exceptions.OptimizationError):
@@ -287,6 +287,6 @@ def test_custom_nonconvex_objective_market_neutral_efficient_risk():
)
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.2309497754562942, target_risk, 1.1102600451243954),
(0.2591296227818582, target_risk, 1.258574109251818),
atol=1e-6,
)

View File

@@ -67,7 +67,9 @@ def test_greedy_allocation_rmse_error():
latest_prices = get_latest_prices(df)
da = DiscreteAllocation(w, latest_prices)
da.greedy_portfolio()
np.testing.assert_almost_equal(da._allocation_rmse_error(), 0.025762032436733803)
np.testing.assert_almost_equal(
da._allocation_rmse_error(verbose=False), 0.017086185150415774
)
def test_greedy_portfolio_allocation_short():
@@ -81,25 +83,25 @@ def test_greedy_portfolio_allocation_short():
da = DiscreteAllocation(w, latest_prices)
allocation, leftover = da.greedy_portfolio()
assert da.allocation == {
"MA": 15,
"PFE": 45,
"FB": 8,
assert allocation == {
"MA": 19,
"PFE": 42,
"FB": 7,
"GOOG": 1,
"BABA": 5,
"AAPL": 4,
"BBY": 8,
"AMZN": 1,
"SBUX": 9,
"WMT": 2,
"XOM": 2,
"SBUX": 8,
"BBY": 6,
"XOM": 4,
"WMT": 3,
"BAC": -32,
"GM": -16,
"GE": -43,
"SHLD": -110,
"AMD": -34,
"JPM": -1,
"T": -1,
"UAA": -1,
"AMD": -48,
"SHLD": -132,
"GM": -9,
"RRC": -19,
"GE": -14,
"T": -5,
"UAA": -8,
}
long_total = 0
short_total = 0
@@ -123,7 +125,9 @@ def test_greedy_allocation_rmse_error_short():
latest_prices = get_latest_prices(df)
da = DiscreteAllocation(w, latest_prices)
da.greedy_portfolio()
np.testing.assert_almost_equal(da._allocation_rmse_error(), 0.033070015016740284)
np.testing.assert_almost_equal(
da._allocation_rmse_error(verbose=False), 0.06063511265243106
)
def test_greedy_portfolio_allocation_short_different_params():
@@ -139,27 +143,26 @@ def test_greedy_portfolio_allocation_short_different_params():
)
allocation, leftover = da.greedy_portfolio()
assert da.allocation == {
"MA": 77,
"PFE": 225,
"FB": 41,
"BABA": 25,
"AAPL": 23,
"BBY": 44,
assert allocation == {
"MA": 96,
"PFE": 211,
"FB": 34,
"GOOG": 4,
"BABA": 22,
"AAPL": 17,
"SBUX": 38,
"AMZN": 2,
"SBUX": 45,
"GOOG": 3,
"WMT": 11,
"XOM": 11,
"BAC": -271,
"GM": -133,
"GE": -356,
"SHLD": -922,
"AMD": -285,
"JPM": -5,
"T": -14,
"UAA": -8,
"RRC": -3,
"BBY": 27,
"XOM": 19,
"WMT": 10,
"BAC": -269,
"AMD": -399,
"SHLD": -1099,
"GM": -78,
"RRC": -154,
"GE": -119,
"T": -41,
"UAA": -64,
}
long_total = 0
short_total = 0
@@ -182,15 +185,15 @@ def test_lp_portfolio_allocation():
da = DiscreteAllocation(w, latest_prices)
allocation, leftover = da.lp_portfolio()
assert da.allocation == {
"AAPL": 5,
"FB": 11,
"BABA": 5,
"AMZN": 1,
"BBY": 7,
"MA": 14,
"PFE": 50,
"SBUX": 5,
assert allocation == {
"GOOG": 1,
"AAPL": 4,
"FB": 12,
"BABA": 4,
"BBY": 2,
"MA": 20,
"PFE": 54,
"SBUX": 1,
}
total = 0
for ticker, num in allocation.items():
@@ -208,7 +211,9 @@ def test_lp_allocation_rmse_error():
latest_prices = get_latest_prices(df)
da = DiscreteAllocation(w, latest_prices)
da.lp_portfolio()
np.testing.assert_almost_equal(da._allocation_rmse_error(verbose=False), 0.017070218149194846)
np.testing.assert_almost_equal(
da._allocation_rmse_error(verbose=False), 0.017082871441954087, decimal=5
)
def test_lp_portfolio_allocation_short():
@@ -222,25 +227,25 @@ def test_lp_portfolio_allocation_short():
da = DiscreteAllocation(w, latest_prices)
allocation, leftover = da.lp_portfolio()
assert da.allocation == {
assert allocation == {
"GOOG": 1,
"AAPL": 5,
"FB": 8,
"AAPL": 4,
"FB": 7,
"BABA": 5,
"WMT": 2,
"XOM": 2,
"BBY": 9,
"MA": 16,
"PFE": 46,
"SBUX": 9,
"GE": -43,
"AMD": -34,
"WMT": 3,
"XOM": 4,
"BBY": 6,
"MA": 19,
"PFE": 42,
"SBUX": 8,
"GE": -14,
"AMD": -48,
"BAC": -32,
"GM": -16,
"T": -1,
"UAA": -1,
"SHLD": -110,
"JPM": -1,
"GM": -9,
"T": -5,
"UAA": -8,
"SHLD": -132,
"RRC": -19,
}
long_total = 0
short_total = 0
@@ -264,7 +269,9 @@ def test_lp_allocation_rmse_error_short():
latest_prices = get_latest_prices(df)
da = DiscreteAllocation(w, latest_prices)
da.lp_portfolio()
np.testing.assert_almost_equal(da._allocation_rmse_error(), 0.027018566693989568)
np.testing.assert_almost_equal(
da._allocation_rmse_error(verbose=False), 0.06063511265243109
)
def test_lp_portfolio_allocation_different_params():
@@ -280,19 +287,18 @@ def test_lp_portfolio_allocation_different_params():
)
allocation, leftover = da.lp_portfolio()
assert da.allocation == {
"GOOG": 1,
"AAPL": 43,
"FB": 95,
"BABA": 44,
"AMZN": 4,
"AMD": 1,
"SHLD": 3,
"BBY": 69,
"MA": 114,
"PFE": 412,
"SBUX": 51,
assert allocation == {
"GOOG": 3,
"AAPL": 32,
"FB": 100,
"BABA": 34,
"AMZN": 2,
"BBY": 15,
"MA": 164,
"PFE": 438,
"SBUX": 15,
}
total = 0
for ticker, num in allocation.items():
total += num * latest_prices[ticker]

View File

@@ -61,7 +61,7 @@ def test_min_volatility():
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.17931232481259154, 0.15915084514118694, 1.00101463282373),
(0.15056821399482578, 0.15915084514118694, 0.8204054077060996),
)
@@ -72,7 +72,7 @@ def test_min_volatility_different_solver():
assert set(w.keys()) == set(ef.tickers)
np.testing.assert_almost_equal(ef.weights.sum(), 1)
assert all([i >= 0 for i in w.values()])
test_performance = (0.179312, 0.159151, 1.001015)
test_performance = (0.150567, 0.159150, 0.820403)
np.testing.assert_allclose(ef.portfolio_performance(), test_performance, atol=1e-5)
ef = setup_efficient_frontier(solver="OSQP")
@@ -124,7 +124,7 @@ def test_min_volatility_short():
np.testing.assert_almost_equal(ef.weights.sum(), 1)
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.1721356467349655, 0.1555915367269669, 0.9777887019776287),
(0.1516319319875544, 0.1555915367269669, 0.8460095886741129),
)
# Shorting should reduce volatility
@@ -156,7 +156,7 @@ def test_min_volatility_L2_reg():
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.23129890623344232, 0.1955254118258614, 1.080672349748733),
(0.17356099329164965, 0.1955254118258614, 0.785376140408869),
)
@@ -205,7 +205,7 @@ def test_min_volatility_tx_costs_L2_reg():
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.2316565265271545, 0.1959773703677164, 1.0800049318450338),
(0.17363446634404042, 0.1959773703677164, 0.7839398296638683),
)
@@ -335,7 +335,7 @@ def test_max_sharpe_long_only():
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.33035037367760506, 0.21671276571944567, 1.4320816434015786),
(0.3047768672819914, 0.22165566922402932, 1.2847714127003216),
)
@@ -380,7 +380,7 @@ def test_max_sharpe_short():
np.testing.assert_almost_equal(ef.weights.sum(), 1)
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.4072439477276246, 0.24823487545231313, 1.5599900981762558),
(0.4937195216716211, 0.29516576454651955, 1.6049270564945908),
)
sharpe = ef.portfolio_performance()[2]
@@ -395,7 +395,7 @@ def test_max_sharpe_L2_reg():
ef = setup_efficient_frontier()
ef.add_objective(objective_functions.L2_reg, gamma=5)
with warnings.catch_warnings(record=True) as w:
with pytest.warns(UserWarning) as w:
weights = ef.max_sharpe()
assert len(w) == 1
@@ -405,7 +405,7 @@ def test_max_sharpe_L2_reg():
assert all([i >= 0 for i in weights.values()])
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.2936875354933478, 0.22783545277575057, 1.2012508683744123),
(0.2516854357026833, 0.22043282695478603, 1.051047790401043),
)
ef2 = setup_efficient_frontier()
@@ -481,7 +481,7 @@ def test_max_sharpe_L2_reg_with_shorts():
np.testing.assert_almost_equal(ef.weights.sum(), 1)
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.3076093180094401, 0.22415982749409985, 1.2830546901496447),
(0.2995338981166366, 0.2234696161770517, 1.2508810052063901),
)
new_number = sum(ef.weights > 0.01)
assert new_number >= initial_number
@@ -733,7 +733,7 @@ def test_max_quadratic_utility():
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.40064324249527605, 0.2917825266124642, 1.3045443362029479),
(0.3677732711751504, 0.2921342197778279, 1.1904571516463793),
)
ret1, var1, _ = ef.portfolio_performance()
@@ -752,7 +752,7 @@ def test_max_quadratic_utility_with_shorts():
np.testing.assert_allclose(
ef.portfolio_performance(),
(1.3318330413711252, 1.0198436183533854, 1.2863080356272452),
(1.4170505733098597, 1.0438577623242156, 1.3383533884915872),
)
@@ -764,7 +764,7 @@ def test_max_quadratic_utility_market_neutral():
np.testing.assert_almost_equal(ef.weights.sum(), 0)
np.testing.assert_allclose(
ef.portfolio_performance(),
(1.13434841843883, 0.9896404148973286, 1.1260134506071473),
(1.248936321062371, 1.0219175004907117, 1.2025787996313317),
)
@@ -789,7 +789,7 @@ def test_max_quadratic_utility_L2_reg():
assert all([i >= 0 for i in weights.values()])
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.2602803268728476, 0.21603540587515674, 1.112226608872166),
(0.19774277217586125, 0.2104822672707046, 0.8444548535162986),
)
ef2 = setup_efficient_frontier()
@@ -820,7 +820,7 @@ def test_efficient_risk():
assert all([i >= 0 for i in w.values()])
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.28577452556155075, 0.19, 1.3988132892376837),
(0.2552422849133517, 0.1900000002876062, 1.2381172871434818),
atol=1e-6,
)
@@ -834,7 +834,7 @@ def test_efficient_risk_error():
assert ef.efficient_risk(min_possible_vol + 0.01)
ef = setup_efficient_frontier()
with pytest.raises(exceptions.OptimizationError):
with pytest.raises(ValueError):
# This volatility is too low
ef.efficient_risk(min_possible_vol - 0.01)
@@ -859,7 +859,7 @@ def test_efficient_risk_short():
np.testing.assert_almost_equal(ef.weights.sum(), 1)
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.30468522897430295, 0.19, 1.4983424153337392),
(0.30035471606347336, 0.1900000003049494, 1.4755511348079207),
atol=1e-6,
)
sharpe = ef.portfolio_performance()[2]
@@ -882,7 +882,7 @@ def test_efficient_risk_L2_reg():
assert all([i >= 0 for i in weights.values()])
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.24087463760460398, 0.19, 1.162498090632486),
(0.1931352562313653, 0.18999999989010993, 0.9112381912184281),
atol=1e-6,
)
@@ -908,7 +908,7 @@ def test_efficient_risk_market_neutral():
assert (ef.weights < 1).all() and (ef.weights > -1).all()
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.2552600197428133, 0.21, 1.1202858085349783),
(0.28640632960825885, 0.20999999995100788, 1.2686015698590967),
atol=1e-6,
)
sharpe = ef.portfolio_performance()[2]
@@ -933,17 +933,16 @@ def test_efficient_risk_market_neutral_L2_reg():
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.10755645826336145, 0.11079556786108302, 0.7902523535340413),
(0.12790320789339854, 0.1175336636355454, 0.9180621496492316),
atol=1e-6,
)
def test_efficient_risk_market_neutral_warning():
ef = setup_efficient_frontier()
with warnings.catch_warnings(record=True) as w:
with pytest.warns(RuntimeWarning) as w:
ef.efficient_risk(0.19, market_neutral=True)
assert len(w) == 1
assert issubclass(w[0].category, RuntimeWarning)
assert (
str(w[0].message)
== "Market neutrality requires shorting - bounds have been amended"
@@ -959,7 +958,7 @@ def test_efficient_return():
assert all([i >= 0 for i in w.values()])
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.25, 0.1738852429895079, 1.3227114391408021),
(0.25, 0.18723269942026335, 1.2284179030274036),
atol=1e-6,
)
@@ -994,7 +993,7 @@ def test_efficient_return_short():
assert set(w.keys()) == set(ef.tickers)
np.testing.assert_almost_equal(ef.weights.sum(), 1)
np.testing.assert_allclose(
ef.portfolio_performance(), (0.25, 0.16826225873038014, 1.3669137793315087)
ef.portfolio_performance(), (0.25, 0.17149595234895817, 1.3411395245760582)
)
sharpe = ef.portfolio_performance()[2]
@@ -1014,7 +1013,7 @@ def test_efficient_return_L2_reg():
np.testing.assert_almost_equal(ef.weights.sum(), 1)
assert all([i >= 0 for i in w.values()])
np.testing.assert_allclose(
ef.portfolio_performance(), (0.25, 0.20033592447690426, 1.1480716731187948)
ef.portfolio_performance(), (0.25, 0.20961660883459776, 1.0972412981906703)
)
@@ -1028,22 +1027,21 @@ def test_efficient_return_market_neutral():
np.testing.assert_almost_equal(ef.weights.sum(), 0)
assert (ef.weights < 1).all() and (ef.weights > -1).all()
np.testing.assert_almost_equal(
ef.portfolio_performance(), (0.25, 0.20567263154580923, 1.1182819914898223)
ef.portfolio_performance(), (0.25, 0.1833060046337015, 1.2547324920403273)
)
sharpe = ef.portfolio_performance()[2]
ef_long_only = setup_efficient_frontier()
ef_long_only.efficient_return(0.25)
long_only_sharpe = ef_long_only.portfolio_performance()[2]
assert long_only_sharpe > sharpe
assert long_only_sharpe < sharpe
def test_efficient_return_market_neutral_warning():
# This fails
ef = setup_efficient_frontier()
with warnings.catch_warnings(record=True) as w:
with pytest.warns(RuntimeWarning) as w:
ef.efficient_return(0.25, market_neutral=True)
assert len(w) == 1
assert issubclass(w[0].category, RuntimeWarning)
assert (
str(w[0].message)
== "Market neutrality requires shorting - bounds have been amended"
@@ -1061,7 +1059,7 @@ def test_max_sharpe_semicovariance():
assert all([i >= 0 for i in w.values()])
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.2972184894480104, 0.06443145011260347, 4.302533762060766),
(0.2732301946250426, 0.06603231922971581, 3.834943215368455),
)
@@ -1077,7 +1075,7 @@ def test_max_sharpe_short_semicovariance():
np.testing.assert_almost_equal(ef.weights.sum(), 1)
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.3564305116656491, 0.07201282488003401, 4.671813836300796),
(0.3907992623559733, 0.0809285460933456, 4.581810501430255),
)
@@ -1097,7 +1095,7 @@ def test_min_volatilty_shrunk_L2_reg():
assert all([i >= 0 for i in w.values()])
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.23127396601517256, 0.19563960638632416, 1.0799140824173181),
(0.17358178582309983, 0.19563960638632416, 0.7850239972361532),
)
@@ -1113,7 +1111,7 @@ def test_efficient_return_shrunk():
np.testing.assert_almost_equal(ef.weights.sum(), 1)
assert all([i >= 0 for i in w.values()])
np.testing.assert_allclose(
ef.portfolio_performance(), (0.22, 0.0849639369932322, 2.353939884117318)
ef.portfolio_performance(), (0.22, 0.08892192396903059, 2.2491641101878916)
)
@@ -1128,7 +1126,7 @@ def test_max_sharpe_exp_cov():
assert all([i >= 0 for i in w.values()])
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.3678817256187322, 0.1753405505478982, 1.9840346373481956),
(0.33700887443850647, 0.1807332515488447, 1.7540152225548384),
)
@@ -1144,7 +1142,7 @@ def test_min_volatility_exp_cov_L2_reg():
assert all([i >= 0 for i in w.values()])
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.2434082300792007, 0.17835412793427002, 1.2526103694192867),
(0.1829496087575576, 0.17835412793427002, 0.9136295898775636),
)
@@ -1161,6 +1159,6 @@ def test_efficient_risk_exp_cov_market_neutral():
assert (ef.weights < 1).all() and (ef.weights > -1).all()
np.testing.assert_allclose(
ef.portfolio_performance(),
(0.3908928033782067, 0.18999999995323363, 1.9520673866815672),
(0.3934093962620499, 0.18999999989011893, 1.9653126130421081),
atol=1e-6,
)

View File

@@ -38,7 +38,7 @@ def test_returns_from_prices():
def test_log_returns_from_prices():
df = get_data()
old_nan = df.isnull().sum(axis=1).sum()
log_rets = expected_returns.log_returns_from_prices(df)
log_rets = expected_returns.returns_from_prices(df, log_returns=True)
new_nan = log_rets.isnull().sum(axis=1).sum()
assert new_nan == old_nan
np.testing.assert_almost_equal(log_rets.iloc[-1, -1], 0.0001682740081102576)
@@ -55,17 +55,12 @@ def test_mean_historical_returns_dummy():
]
)
mean = expected_returns.mean_historical_return(data, frequency=1)
test_answer = pd.Series([0.00865598, 0.025, 0.01286968, -0.03632333])
pd.testing.assert_series_equal(mean, test_answer)
mean = expected_returns.mean_historical_return(data, compounding=True, frequency=1)
pd.testing.assert_series_equal(mean, test_answer)
test_answer = pd.Series([0.0061922, 0.0241137, 0.0122722, -0.0421775])
pd.testing.assert_series_equal(mean, test_answer, check_less_precise=4)
def test_mean_historical_returns_compounding():
df = get_data()
mean = expected_returns.mean_historical_return(df)
mean2 = expected_returns.mean_historical_return(df, compounding=True)
assert (mean2 >= mean).all()
mean = expected_returns.mean_historical_return(data, compounding=False, frequency=1)
test_answer = pd.Series([0.0086560, 0.0250000, 0.0128697, -0.03632333])
pd.testing.assert_series_equal(mean, test_answer, check_less_precise=4)
def test_mean_historical_returns():
@@ -77,26 +72,26 @@ def test_mean_historical_returns():
assert mean.dtype == "float64"
correct_mean = np.array(
[
0.26770284,
0.3637864,
0.31709032,
0.22616723,
0.49982007,
0.16888704,
0.22754479,
0.14783539,
0.19001915,
0.08150653,
0.12826351,
0.25797816,
0.07580128,
0.16087243,
0.20510267,
0.3511536,
0.38808003,
0.24635612,
0.21798433,
0.28474973,
0.247967,
0.294304,
0.284037,
0.1923164,
0.371327,
0.1360093,
0.0328503,
0.1200115,
0.105540,
0.0423457,
0.1002559,
0.1442237,
-0.0792602,
0.1430506,
0.0736356,
0.238835,
0.388665,
0.226717,
0.1561701,
0.2318153,
]
)
np.testing.assert_array_almost_equal(mean.values, correct_mean)
@@ -106,10 +101,9 @@ def test_mean_historical_returns_type_warning():
df = get_data()
mean = expected_returns.mean_historical_return(df)
with warnings.catch_warnings(record=True) as w:
with pytest.warns(RuntimeWarning) as w:
mean_from_array = expected_returns.mean_historical_return(np.array(df))
assert len(w) == 1
assert issubclass(w[0].category, RuntimeWarning)
assert str(w[0].message) == "prices are not in a dataframe"
np.testing.assert_array_almost_equal(mean.values, mean_from_array.values, decimal=6)
@@ -117,8 +111,8 @@ def test_mean_historical_returns_type_warning():
def test_mean_historical_returns_frequency():
df = get_data()
mean = expected_returns.mean_historical_return(df)
mean2 = expected_returns.mean_historical_return(df, frequency=52)
mean = expected_returns.mean_historical_return(df, compounding=False)
mean2 = expected_returns.mean_historical_return(df, compounding=False, frequency=52)
np.testing.assert_array_almost_equal(mean / 252, mean2 / 52)
@@ -133,56 +127,18 @@ def test_ema_historical_return():
def test_ema_historical_return_frequency():
df = get_data()
mean = expected_returns.ema_historical_return(df)
mean2 = expected_returns.ema_historical_return(df, frequency=52)
mean = expected_returns.ema_historical_return(df, compounding=False)
mean2 = expected_returns.ema_historical_return(df, compounding=False, frequency=52)
np.testing.assert_array_almost_equal(mean / 252, mean2 / 52)
mean3 = expected_returns.ema_historical_return(df, compounding=True)
assert (abs(mean3) > mean).all()
def test_ema_historical_return_limit():
df = get_data()
sma = expected_returns.mean_historical_return(df)
ema = expected_returns.ema_historical_return(df, span=1e10)
sma = expected_returns.mean_historical_return(df, compounding=False)
ema = expected_returns.ema_historical_return(df, compounding=False, span=1e10)
np.testing.assert_array_almost_equal(ema.values, sma.values)
def test_james_stein():
df = get_data()
js = expected_returns.james_stein_shrinkage(df)
correct_mean = np.array(
[
0.25870218,
0.32318595,
0.29184719,
0.23082673,
0.41448111,
0.19238474,
0.23175124,
0.17825652,
0.20656697,
0.13374178,
0.16512141,
0.25217574,
0.12991287,
0.18700597,
0.21668984,
0.3147078,
0.33948993,
0.24437593,
0.225335,
0.27014272,
]
)
np.testing.assert_array_almost_equal(js.values, correct_mean)
# Test shrinkage
y = expected_returns.returns_from_prices(df).mean(axis=0) * 252
nu = y.mean()
assert (((js <= nu) & (js >= y)) | ((js >= nu) & (js <= y))).all()
def test_capm_no_benchmark():
df = get_data()
mu = expected_returns.capm_return(df)
@@ -192,26 +148,26 @@ def test_capm_no_benchmark():
assert mu.dtype == "float64"
correct_mu = np.array(
[
0.21803135,
0.27902605,
0.14475533,
0.14668971,
0.40944875,
0.22361704,
0.39057166,
0.164807,
0.31280876,
0.17018046,
0.15044284,
0.34609161,
0.3233097,
0.1479624,
0.26403991,
0.31124465,
0.27312086,
0.16703193,
0.30396023,
0.25182927,
0.22148462799238577,
0.2835429647498704,
0.14693081977908462,
0.1488989354304723,
0.4162399750335195,
0.22716772604184535,
0.3970337136813829,
0.16733214988182069,
0.31791477659742146,
0.17279931642386534,
0.15271750464365566,
0.351778014382922,
0.32859883451716376,
0.1501938182844417,
0.268295486802897,
0.31632339201710874,
0.27753479916328516,
0.16959588523287855,
0.3089119447773357,
0.2558719211959501,
]
)
np.testing.assert_array_almost_equal(mu.values, correct_mu)
@@ -228,26 +184,26 @@ def test_capm_with_benchmark():
assert mu.dtype == "float64"
correct_mu = np.array(
[
0.10903299,
0.11891232,
0.0659977,
0.07369941,
0.15948144,
0.12308759,
0.15907944,
0.08680978,
0.15778843,
0.0903294,
0.09043133,
0.14716681,
0.12510181,
0.0927869,
0.10990104,
0.12317033,
0.13596521,
0.09344662,
0.15457909,
0.11430041,
0.09115799375654746,
0.09905386632033128,
0.05676282405265752,
0.06291827346436336,
0.13147799781014877,
0.10239088012000815,
0.1311567086884512,
0.07339649698626659,
0.1301248935078549,
0.07620949056643983,
0.07629095442513395,
0.12163575425541985,
0.10400070536161658,
0.0781736030988492,
0.09185177050469516,
0.10245700691271296,
0.11268307946677197,
0.07870087187919145,
0.1275598841214107,
0.09536788741392595,
]
)
np.testing.assert_array_almost_equal(mu.values, correct_mu)
@@ -257,12 +213,7 @@ def test_risk_matrix_and_returns_data():
# Test the switcher method for simple calls
df = get_data()
for method in {
"mean_historical_return",
"ema_historical_return",
"james_stein_shrinkage",
"capm_return",
}:
for method in {"mean_historical_return", "ema_historical_return", "capm_return"}:
mu = expected_returns.return_model(df, method=method)
assert isinstance(mu, pd.Series)

View File

@@ -10,7 +10,7 @@ def test_hrp_portfolio():
df = get_data()
returns = df.pct_change().dropna(how="all")
hrp = HRPOpt(returns)
w = hrp.optimize()
w = hrp.optimize(linkage_method="single")
# uncomment this line if you want generating a new file
# pd.Series(w).to_csv(resource("weights_hrp.csv"))
@@ -32,7 +32,7 @@ def test_portfolio_performance():
hrp = HRPOpt(returns)
with pytest.raises(ValueError):
hrp.portfolio_performance()
hrp.optimize()
hrp.optimize(linkage_method="single")
np.testing.assert_allclose(
hrp.portfolio_performance(),
(0.21353402380950973, 0.17844159743748936, 1.084579081272277),
@@ -43,7 +43,7 @@ def test_pass_cov_matrix():
df = get_data()
S = CovarianceShrinkage(df).ledoit_wolf()
hrp = HRPOpt(cov_matrix=S)
hrp.optimize()
hrp.optimize(linkage_method="single")
perf = hrp.portfolio_performance()
assert perf[0] is None and perf[2] is None
np.testing.assert_almost_equal(perf[1], 0.10002783894982334)
@@ -62,6 +62,6 @@ def test_quasi_dag():
df = get_data()
returns = df.pct_change().dropna(how="all")
hrp = HRPOpt(returns)
hrp.optimize()
hrp.optimize(linkage_method="single")
clusters = hrp.clusters
assert HRPOpt._get_quasi_diag(clusters)[:5] == [12, 6, 15, 14, 2]

View File

@@ -36,9 +36,9 @@ def test_ef_plot():
cla = CLA(rets, S)
ax = plotting.plot_efficient_frontier(cla, showfig=False)
assert len(ax.findobj()) == 137
assert len(ax.findobj()) == 143
ax = plotting.plot_efficient_frontier(cla, show_assets=False, showfig=False)
assert len(ax.findobj()) == 149
assert len(ax.findobj()) == 161
def test_weight_plot():

View File

@@ -27,6 +27,11 @@ def test_sample_cov_dummy():
pd.testing.assert_frame_equal(S, test_answer)
def test_is_positive_semidefinite():
a = np.zeros((100, 100))
assert risk_models._is_positive_semidefinite(a)
def test_sample_cov_real_data():
df = get_data()
S = risk_models.sample_cov(df)
@@ -42,11 +47,9 @@ def test_sample_cov_type_warning():
cov_from_df = risk_models.sample_cov(df)
returns_as_array = np.array(df)
with warnings.catch_warnings(record=True) as w:
with pytest.warns(RuntimeWarning) as w:
cov_from_array = risk_models.sample_cov(returns_as_array)
assert len(w) == 1
assert issubclass(w[0].category, RuntimeWarning)
assert str(w[0].message) == "data is not in a dataframe"
np.testing.assert_array_almost_equal(
@@ -59,11 +62,10 @@ def test_sample_cov_npd():
assert not risk_models._is_positive_semidefinite(S)
for method in {"spectral", "diag"}:
with warnings.catch_warnings(record=True) as w:
with pytest.warns(UserWarning) as w:
S2 = risk_models.fix_nonpositive_semidefinite(S, fix_method=method)
assert risk_models._is_positive_semidefinite(S2)
assert len(w) == 1
assert issubclass(w[0].category, UserWarning)
assert (
str(w[0].message)
== "The covariance matrix is non positive semidefinite. Amending eigenvalues."
@@ -148,10 +150,9 @@ def test_cov_to_corr():
test_corr = risk_models.cov_to_corr(rets.cov())
pd.testing.assert_frame_equal(test_corr, rets.corr())
with warnings.catch_warnings(record=True) as w:
with pytest.warns(RuntimeWarning) as w:
test_corr_numpy = risk_models.cov_to_corr(rets.cov().values)
assert len(w) == 1
assert issubclass(w[0].category, RuntimeWarning)
assert str(w[0].message) == "cov_matrix is not a dataframe"
np.testing.assert_array_almost_equal(test_corr_numpy, rets.corr().values)