openseries 1.4.12__tar.gz → 1.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openseries-1.4.12 → openseries-1.5.1}/PKG-INFO +28 -27
- {openseries-1.4.12 → openseries-1.5.1}/README.md +26 -25
- {openseries-1.4.12 → openseries-1.5.1}/openseries/_common_model.py +1 -1
- {openseries-1.4.12 → openseries-1.5.1}/openseries/_risk.py +0 -3
- {openseries-1.4.12 → openseries-1.5.1}/openseries/frame.py +582 -62
- {openseries-1.4.12 → openseries-1.5.1}/openseries/series.py +1 -0
- {openseries-1.4.12 → openseries-1.5.1}/openseries/simulation.py +8 -2
- {openseries-1.4.12 → openseries-1.5.1}/openseries/types.py +0 -2
- {openseries-1.4.12 → openseries-1.5.1}/pyproject.toml +15 -17
- {openseries-1.4.12 → openseries-1.5.1}/LICENSE.md +0 -0
- {openseries-1.4.12 → openseries-1.5.1}/openseries/__init__.py +0 -0
- {openseries-1.4.12 → openseries-1.5.1}/openseries/datefixer.py +0 -0
- {openseries-1.4.12 → openseries-1.5.1}/openseries/load_plotly.py +0 -0
- {openseries-1.4.12 → openseries-1.5.1}/openseries/plotly_captor_logo.json +0 -0
- {openseries-1.4.12 → openseries-1.5.1}/openseries/plotly_layouts.json +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: openseries
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.5.1
|
4
4
|
Summary: Package for analyzing financial timeseries.
|
5
5
|
Home-page: https://github.com/CaptorAB/OpenSeries
|
6
6
|
License: BSD-3-Clause
|
@@ -20,7 +20,6 @@ Classifier: Programming Language :: Python :: 3.10
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.11
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
22
22
|
Classifier: Topic :: Office/Business :: Financial :: Investment
|
23
|
-
Requires-Dist: ffn (>=1.0.0,<2.0.0)
|
24
23
|
Requires-Dist: holidays (>=0.30,<1.0)
|
25
24
|
Requires-Dist: numpy (>=1.23.2,<=2.0.0)
|
26
25
|
Requires-Dist: openpyxl (>=3.1.2,<4.0.0)
|
@@ -29,6 +28,7 @@ Requires-Dist: plotly (>=5.18.0,<6.0.0)
|
|
29
28
|
Requires-Dist: pyarrow (>=14.0.2,<16.0.0)
|
30
29
|
Requires-Dist: pydantic (>=2.5.2,<3.0.0)
|
31
30
|
Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
|
31
|
+
Requires-Dist: requests (>=2.20.0,<3.0.0)
|
32
32
|
Requires-Dist: scipy (>=1.11.4,<2.0.0)
|
33
33
|
Requires-Dist: statsmodels (>=0.14.0,<1.0.0)
|
34
34
|
Project-URL: Repository, https://github.com/CaptorAB/OpenSeries
|
@@ -43,12 +43,13 @@ width="81" height="100" align="left" float="right"/><br/>
|
|
43
43
|
# OpenSeries
|
44
44
|
|
45
45
|
[](https://pypi.org/project/openseries/)
|
46
|
-
[](https://anaconda.org/conda-forge/openseries)
|
47
|
+
[](https://anaconda.org/conda-forge/openseries)
|
48
48
|
[](https://www.python.org/)
|
49
49
|
[](https://github.com/CaptorAB/OpenSeries/actions/workflows/test.yml)
|
50
50
|
[](https://github.com/CaptorAB/OpenSeries/actions/workflows/test.yml)
|
51
51
|
[](https://github.com/CaptorAB/OpenSeries/actions/workflows/check.yml)
|
52
|
+
[](https://python-poetry.org/)
|
52
53
|
[](https://beta.ruff.rs/docs/)
|
53
54
|
[](https://opensource.org/licenses/BSD-3-Clause)
|
54
55
|
|
@@ -198,11 +199,11 @@ make lint
|
|
198
199
|
|
199
200
|
### On some files in the project
|
200
201
|
|
201
|
-
| File | Description
|
202
|
-
|
203
|
-
| [series.py](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/series.py) | Defines the class _OpenTimeSeries_ for managing and analyzing a single timeseries. The module also defines a function `timeseries_chain` that can be used to chain two timeseries objects together.
|
204
|
-
| [frame.py](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/frame.py) | Defines the class _OpenFrame_ for managing a group of timeseries, and e.g. calculate a portfolio timeseries from a rebalancing strategy between timeseries.
|
205
|
-
| [simulation.py](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/simulation.py) | Defines the class _ReturnSimulation_ to create simulated financial timeseries. Used in the project's test suite
|
202
|
+
| File | Description |
|
203
|
+
|:-----------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
204
|
+
| [series.py](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/series.py) | Defines the class _OpenTimeSeries_ for managing and analyzing a single timeseries. The module also defines a function `timeseries_chain` that can be used to chain two timeseries objects together. |
|
205
|
+
| [frame.py](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/frame.py) | Defines the class _OpenFrame_ for managing a group of timeseries, and e.g. calculate a portfolio timeseries from a rebalancing strategy between timeseries. The module also defines functions to simulate, optimize, and plot portfolios. |
|
206
|
+
| [simulation.py](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/simulation.py) | Defines the class _ReturnSimulation_ to create simulated financial timeseries. Used in the project's test suite |
|
206
207
|
|
207
208
|
### Class methods used to construct objects.
|
208
209
|
|
@@ -271,24 +272,24 @@ make lint
|
|
271
272
|
|
272
273
|
### Methods that apply only to the [OpenFrame](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/frame.py) class.
|
273
274
|
|
274
|
-
| Method | Applies to | Description
|
275
|
-
|
276
|
-
| `merge_series` | `OpenFrame` | Merges the Pandas Dataframes of the constituent OpenTimeSeries.
|
277
|
-
| `trunc_frame` | `OpenFrame` | Truncates the OpenFrame to a common period.
|
278
|
-
| `add_timeseries` | `OpenFrame` | Adds a given OpenTimeSeries to the OpenFrame.
|
279
|
-
| `delete_timeseries` | `OpenFrame` | Deletes an OpenTimeSeries from the OpenFrame.
|
280
|
-
| `relative` | `OpenFrame` | Calculates a new series that is the relative performance of two others.
|
281
|
-
| `make_portfolio` | `OpenFrame` | Calculates a portfolio timeseries based on the series and weights. Weights can be provided as a list, or a weight strategy can be
|
282
|
-
| `ord_least_squares_fit` | `OpenFrame` | Performs a regression and an [Ordinary Least Squares](https://www.statsmodels.org/stable/examples/notebooks/generated/ols.html) fit.
|
283
|
-
| `beta` | `OpenFrame` | Calculates [Beta](https://www.investopedia.com/terms/b/beta.asp) of an asset relative a market.
|
284
|
-
| `jensen_alpha` | `OpenFrame` | Calculates [Jensen's Alpha](https://www.investopedia.com/terms/j/jensensmeasure.asp) of an asset relative a market.
|
285
|
-
| `tracking_error_func` | `OpenFrame` | Calculates the [tracking errors](https://www.investopedia.com/terms/t/trackingerror.asp) relative to a selected series in the OpenFrame.
|
286
|
-
| `info_ratio_func` | `OpenFrame` | Calculates the [information ratios](https://www.investopedia.com/terms/i/informationratio.asp) relative to a selected series in the OpenFrame.
|
287
|
-
| `capture_ratio_func` | `OpenFrame` | Calculates up, down and up/down [capture ratios](https://www.investopedia.com/terms/d/down-market-capture-ratio.asp) relative to a selected series.
|
288
|
-
| `rolling_info_ratio` | `OpenFrame` | Returns a pandas.DataFrame with the rolling [information ratio](https://www.investopedia.com/terms/i/informationratio.asp) between two series.
|
289
|
-
| `rolling_beta` | `OpenFrame` | Returns a pandas.DataFrame with the rolling [Beta](https://www.investopedia.com/terms/b/beta.asp) of an asset relative a market.
|
290
|
-
| `rolling_corr` | `OpenFrame` | Calculates and adds a series of rolling [correlations](https://www.investopedia.com/terms/c/correlation.asp) between two other series.
|
291
|
-
| `ewma_risk` | `OpenFrame` | Returns a `pandas.DataFrame` with volatility and correlation based on [Exponentially Weighted Moving Average](https://www.investopedia.com/articles/07/ewma.asp).
|
275
|
+
| Method | Applies to | Description |
|
276
|
+
|:------------------------|:------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
277
|
+
| `merge_series` | `OpenFrame` | Merges the Pandas Dataframes of the constituent OpenTimeSeries. |
|
278
|
+
| `trunc_frame` | `OpenFrame` | Truncates the OpenFrame to a common period. |
|
279
|
+
| `add_timeseries` | `OpenFrame` | Adds a given OpenTimeSeries to the OpenFrame. |
|
280
|
+
| `delete_timeseries` | `OpenFrame` | Deletes an OpenTimeSeries from the OpenFrame. |
|
281
|
+
| `relative` | `OpenFrame` | Calculates a new series that is the relative performance of two others. |
|
282
|
+
| `make_portfolio` | `OpenFrame` | Calculates a portfolio timeseries based on the series and weights. Weights can be provided as a list, or a weight strategy can be set as *equal weights* or *inverted volatility*. |
|
283
|
+
| `ord_least_squares_fit` | `OpenFrame` | Performs a regression and an [Ordinary Least Squares](https://www.statsmodels.org/stable/examples/notebooks/generated/ols.html) fit. |
|
284
|
+
| `beta` | `OpenFrame` | Calculates [Beta](https://www.investopedia.com/terms/b/beta.asp) of an asset relative a market. |
|
285
|
+
| `jensen_alpha` | `OpenFrame` | Calculates [Jensen's Alpha](https://www.investopedia.com/terms/j/jensensmeasure.asp) of an asset relative a market. |
|
286
|
+
| `tracking_error_func` | `OpenFrame` | Calculates the [tracking errors](https://www.investopedia.com/terms/t/trackingerror.asp) relative to a selected series in the OpenFrame. |
|
287
|
+
| `info_ratio_func` | `OpenFrame` | Calculates the [information ratios](https://www.investopedia.com/terms/i/informationratio.asp) relative to a selected series in the OpenFrame. |
|
288
|
+
| `capture_ratio_func` | `OpenFrame` | Calculates up, down and up/down [capture ratios](https://www.investopedia.com/terms/d/down-market-capture-ratio.asp) relative to a selected series. |
|
289
|
+
| `rolling_info_ratio` | `OpenFrame` | Returns a pandas.DataFrame with the rolling [information ratio](https://www.investopedia.com/terms/i/informationratio.asp) between two series. |
|
290
|
+
| `rolling_beta` | `OpenFrame` | Returns a pandas.DataFrame with the rolling [Beta](https://www.investopedia.com/terms/b/beta.asp) of an asset relative a market. |
|
291
|
+
| `rolling_corr` | `OpenFrame` | Calculates and adds a series of rolling [correlations](https://www.investopedia.com/terms/c/correlation.asp) between two other series. |
|
292
|
+
| `ewma_risk` | `OpenFrame` | Returns a `pandas.DataFrame` with volatility and correlation based on [Exponentially Weighted Moving Average](https://www.investopedia.com/articles/07/ewma.asp). |
|
292
293
|
|
293
294
|
### Methods that apply to both the [OpenTimeSeries](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/series.py) and the [OpenFrame](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/frame.py) class.
|
294
295
|
|
@@ -7,12 +7,13 @@ width="81" height="100" align="left" float="right"/><br/>
|
|
7
7
|
# OpenSeries
|
8
8
|
|
9
9
|
[](https://pypi.org/project/openseries/)
|
10
|
-
[](https://anaconda.org/conda-forge/openseries)
|
11
|
+
[](https://anaconda.org/conda-forge/openseries)
|
12
12
|
[](https://www.python.org/)
|
13
13
|
[](https://github.com/CaptorAB/OpenSeries/actions/workflows/test.yml)
|
14
14
|
[](https://github.com/CaptorAB/OpenSeries/actions/workflows/test.yml)
|
15
15
|
[](https://github.com/CaptorAB/OpenSeries/actions/workflows/check.yml)
|
16
|
+
[](https://python-poetry.org/)
|
16
17
|
[](https://beta.ruff.rs/docs/)
|
17
18
|
[](https://opensource.org/licenses/BSD-3-Clause)
|
18
19
|
|
@@ -162,11 +163,11 @@ make lint
|
|
162
163
|
|
163
164
|
### On some files in the project
|
164
165
|
|
165
|
-
| File | Description
|
166
|
-
|
167
|
-
| [series.py](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/series.py) | Defines the class _OpenTimeSeries_ for managing and analyzing a single timeseries. The module also defines a function `timeseries_chain` that can be used to chain two timeseries objects together.
|
168
|
-
| [frame.py](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/frame.py) | Defines the class _OpenFrame_ for managing a group of timeseries, and e.g. calculate a portfolio timeseries from a rebalancing strategy between timeseries.
|
169
|
-
| [simulation.py](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/simulation.py) | Defines the class _ReturnSimulation_ to create simulated financial timeseries. Used in the project's test suite
|
166
|
+
| File | Description |
|
167
|
+
|:-----------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
168
|
+
| [series.py](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/series.py) | Defines the class _OpenTimeSeries_ for managing and analyzing a single timeseries. The module also defines a function `timeseries_chain` that can be used to chain two timeseries objects together. |
|
169
|
+
| [frame.py](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/frame.py) | Defines the class _OpenFrame_ for managing a group of timeseries, and e.g. calculate a portfolio timeseries from a rebalancing strategy between timeseries. The module also defines functions to simulate, optimize, and plot portfolios. |
|
170
|
+
| [simulation.py](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/simulation.py) | Defines the class _ReturnSimulation_ to create simulated financial timeseries. Used in the project's test suite |
|
170
171
|
|
171
172
|
### Class methods used to construct objects.
|
172
173
|
|
@@ -235,24 +236,24 @@ make lint
|
|
235
236
|
|
236
237
|
### Methods that apply only to the [OpenFrame](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/frame.py) class.
|
237
238
|
|
238
|
-
| Method | Applies to | Description
|
239
|
-
|
240
|
-
| `merge_series` | `OpenFrame` | Merges the Pandas Dataframes of the constituent OpenTimeSeries.
|
241
|
-
| `trunc_frame` | `OpenFrame` | Truncates the OpenFrame to a common period.
|
242
|
-
| `add_timeseries` | `OpenFrame` | Adds a given OpenTimeSeries to the OpenFrame.
|
243
|
-
| `delete_timeseries` | `OpenFrame` | Deletes an OpenTimeSeries from the OpenFrame.
|
244
|
-
| `relative` | `OpenFrame` | Calculates a new series that is the relative performance of two others.
|
245
|
-
| `make_portfolio` | `OpenFrame` | Calculates a portfolio timeseries based on the series and weights. Weights can be provided as a list, or a weight strategy can be
|
246
|
-
| `ord_least_squares_fit` | `OpenFrame` | Performs a regression and an [Ordinary Least Squares](https://www.statsmodels.org/stable/examples/notebooks/generated/ols.html) fit.
|
247
|
-
| `beta` | `OpenFrame` | Calculates [Beta](https://www.investopedia.com/terms/b/beta.asp) of an asset relative a market.
|
248
|
-
| `jensen_alpha` | `OpenFrame` | Calculates [Jensen's Alpha](https://www.investopedia.com/terms/j/jensensmeasure.asp) of an asset relative a market.
|
249
|
-
| `tracking_error_func` | `OpenFrame` | Calculates the [tracking errors](https://www.investopedia.com/terms/t/trackingerror.asp) relative to a selected series in the OpenFrame.
|
250
|
-
| `info_ratio_func` | `OpenFrame` | Calculates the [information ratios](https://www.investopedia.com/terms/i/informationratio.asp) relative to a selected series in the OpenFrame.
|
251
|
-
| `capture_ratio_func` | `OpenFrame` | Calculates up, down and up/down [capture ratios](https://www.investopedia.com/terms/d/down-market-capture-ratio.asp) relative to a selected series.
|
252
|
-
| `rolling_info_ratio` | `OpenFrame` | Returns a pandas.DataFrame with the rolling [information ratio](https://www.investopedia.com/terms/i/informationratio.asp) between two series.
|
253
|
-
| `rolling_beta` | `OpenFrame` | Returns a pandas.DataFrame with the rolling [Beta](https://www.investopedia.com/terms/b/beta.asp) of an asset relative a market.
|
254
|
-
| `rolling_corr` | `OpenFrame` | Calculates and adds a series of rolling [correlations](https://www.investopedia.com/terms/c/correlation.asp) between two other series.
|
255
|
-
| `ewma_risk` | `OpenFrame` | Returns a `pandas.DataFrame` with volatility and correlation based on [Exponentially Weighted Moving Average](https://www.investopedia.com/articles/07/ewma.asp).
|
239
|
+
| Method | Applies to | Description |
|
240
|
+
|:------------------------|:------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
241
|
+
| `merge_series` | `OpenFrame` | Merges the Pandas Dataframes of the constituent OpenTimeSeries. |
|
242
|
+
| `trunc_frame` | `OpenFrame` | Truncates the OpenFrame to a common period. |
|
243
|
+
| `add_timeseries` | `OpenFrame` | Adds a given OpenTimeSeries to the OpenFrame. |
|
244
|
+
| `delete_timeseries` | `OpenFrame` | Deletes an OpenTimeSeries from the OpenFrame. |
|
245
|
+
| `relative` | `OpenFrame` | Calculates a new series that is the relative performance of two others. |
|
246
|
+
| `make_portfolio` | `OpenFrame` | Calculates a portfolio timeseries based on the series and weights. Weights can be provided as a list, or a weight strategy can be set as *equal weights* or *inverted volatility*. |
|
247
|
+
| `ord_least_squares_fit` | `OpenFrame` | Performs a regression and an [Ordinary Least Squares](https://www.statsmodels.org/stable/examples/notebooks/generated/ols.html) fit. |
|
248
|
+
| `beta` | `OpenFrame` | Calculates [Beta](https://www.investopedia.com/terms/b/beta.asp) of an asset relative a market. |
|
249
|
+
| `jensen_alpha` | `OpenFrame` | Calculates [Jensen's Alpha](https://www.investopedia.com/terms/j/jensensmeasure.asp) of an asset relative a market. |
|
250
|
+
| `tracking_error_func` | `OpenFrame` | Calculates the [tracking errors](https://www.investopedia.com/terms/t/trackingerror.asp) relative to a selected series in the OpenFrame. |
|
251
|
+
| `info_ratio_func` | `OpenFrame` | Calculates the [information ratios](https://www.investopedia.com/terms/i/informationratio.asp) relative to a selected series in the OpenFrame. |
|
252
|
+
| `capture_ratio_func` | `OpenFrame` | Calculates up, down and up/down [capture ratios](https://www.investopedia.com/terms/d/down-market-capture-ratio.asp) relative to a selected series. |
|
253
|
+
| `rolling_info_ratio` | `OpenFrame` | Returns a pandas.DataFrame with the rolling [information ratio](https://www.investopedia.com/terms/i/informationratio.asp) between two series. |
|
254
|
+
| `rolling_beta` | `OpenFrame` | Returns a pandas.DataFrame with the rolling [Beta](https://www.investopedia.com/terms/b/beta.asp) of an asset relative a market. |
|
255
|
+
| `rolling_corr` | `OpenFrame` | Calculates and adds a series of rolling [correlations](https://www.investopedia.com/terms/c/correlation.asp) between two other series. |
|
256
|
+
| `ewma_risk` | `OpenFrame` | Returns a `pandas.DataFrame` with volatility and correlation based on [Exponentially Weighted Moving Average](https://www.investopedia.com/articles/07/ewma.asp). |
|
256
257
|
|
257
258
|
### Methods that apply to both the [OpenTimeSeries](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/series.py) and the [OpenFrame](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/frame.py) class.
|
258
259
|
|
@@ -124,9 +124,6 @@ def _calc_inv_vol_weights(returns: DataFrame) -> NDArray[float64]:
|
|
124
124
|
"""
|
125
125
|
Calculate weights proportional to inverse volatility.
|
126
126
|
|
127
|
-
Source: https://github.com/pmorissette/ffn.
|
128
|
-
Function copied here because of FutureWarning from pandas ^2.1.0
|
129
|
-
|
130
127
|
Parameters
|
131
128
|
----------
|
132
129
|
returns: pandas.DataFrame
|
@@ -1,19 +1,35 @@
|
|
1
1
|
"""Defining the OpenFrame class."""
|
2
|
+
|
2
3
|
# mypy: disable-error-code="index,assignment"
|
3
4
|
from __future__ import annotations
|
4
5
|
|
5
6
|
import datetime as dt
|
6
7
|
from copy import deepcopy
|
7
8
|
from functools import reduce
|
9
|
+
from inspect import stack
|
8
10
|
from logging import warning
|
9
|
-
from
|
11
|
+
from pathlib import Path
|
12
|
+
from typing import Callable, Optional, Union, cast
|
10
13
|
|
11
14
|
import statsmodels.api as sm # type: ignore[import-untyped,unused-ignore]
|
12
|
-
from
|
13
|
-
|
14
|
-
|
15
|
+
from numpy import (
|
16
|
+
append,
|
17
|
+
array,
|
18
|
+
cov,
|
19
|
+
cumprod,
|
20
|
+
dot,
|
21
|
+
float64,
|
22
|
+
inf,
|
23
|
+
linspace,
|
24
|
+
log,
|
25
|
+
nan,
|
26
|
+
sqrt,
|
27
|
+
zeros,
|
28
|
+
)
|
29
|
+
from numpy import (
|
30
|
+
sum as npsum,
|
15
31
|
)
|
16
|
-
from numpy import
|
32
|
+
from numpy.typing import NDArray
|
17
33
|
from pandas import (
|
18
34
|
DataFrame,
|
19
35
|
DatetimeIndex,
|
@@ -24,7 +40,11 @@ from pandas import (
|
|
24
40
|
concat,
|
25
41
|
merge,
|
26
42
|
)
|
27
|
-
from
|
43
|
+
from plotly.graph_objs import Figure # type: ignore[import-untyped,unused-ignore]
|
44
|
+
from plotly.io import to_html # type: ignore[import-untyped,unused-ignore]
|
45
|
+
from plotly.offline import plot # type: ignore[import-untyped,unused-ignore]
|
46
|
+
from pydantic import DirectoryPath, field_validator
|
47
|
+
from scipy.optimize import minimize # type: ignore[import-untyped,unused-ignore]
|
28
48
|
|
29
49
|
# noinspection PyProtectedMember
|
30
50
|
from statsmodels.regression.linear_model import ( # type: ignore[import-untyped,unused-ignore]
|
@@ -38,26 +58,30 @@ from openseries._risk import (
|
|
38
58
|
_ewma_calc,
|
39
59
|
)
|
40
60
|
from openseries.datefixer import do_resample_to_business_period_ends
|
61
|
+
from openseries.load_plotly import load_plotly_dict
|
41
62
|
from openseries.series import OpenTimeSeries
|
63
|
+
from openseries.simulation import random_generator
|
42
64
|
from openseries.types import (
|
43
65
|
CountriesType,
|
44
66
|
DaysInYearType,
|
45
67
|
LiteralBizDayFreq,
|
46
68
|
LiteralCaptureRatio,
|
47
|
-
LiteralCovMethod,
|
48
69
|
LiteralFrameProps,
|
49
70
|
LiteralHowMerge,
|
71
|
+
LiteralLinePlotMode,
|
50
72
|
LiteralOlsFitCovType,
|
51
73
|
LiteralOlsFitMethod,
|
52
74
|
LiteralPandasReindexMethod,
|
75
|
+
LiteralPlotlyJSlib,
|
76
|
+
LiteralPlotlyOutput,
|
53
77
|
LiteralPortfolioWeightings,
|
54
|
-
LiteralRiskParityMethod,
|
55
78
|
LiteralTrunc,
|
56
79
|
OpenFramePropertiesList,
|
57
80
|
ValueType,
|
58
81
|
)
|
59
82
|
|
60
83
|
|
84
|
+
# noinspection PyUnresolvedReferences
|
61
85
|
class OpenFrame(_CommonModel):
|
62
86
|
|
63
87
|
"""
|
@@ -220,7 +244,7 @@ class OpenFrame(_CommonModel):
|
|
220
244
|
prop_list = [
|
221
245
|
getattr(self, x) for x in OpenFramePropertiesList.allowed_strings
|
222
246
|
]
|
223
|
-
return concat(prop_list, axis="columns").T
|
247
|
+
return cast(DataFrame, concat(prop_list, axis="columns").T)
|
224
248
|
|
225
249
|
@property
|
226
250
|
def lengths_of_items(self: Self) -> Series[int]:
|
@@ -458,8 +482,8 @@ class OpenFrame(_CommonModel):
|
|
458
482
|
tail = self.tsdf.loc[self.last_indices.min()].copy()
|
459
483
|
dates = do_resample_to_business_period_ends(
|
460
484
|
data=self.tsdf,
|
461
|
-
head=head, # type: ignore[arg-type]
|
462
|
-
tail=tail, # type: ignore[arg-type]
|
485
|
+
head=head, # type: ignore[arg-type,unused-ignore]
|
486
|
+
tail=tail, # type: ignore[arg-type,unused-ignore]
|
463
487
|
freq=freq,
|
464
488
|
countries=countries,
|
465
489
|
)
|
@@ -637,6 +661,7 @@ class OpenFrame(_CommonModel):
|
|
637
661
|
|
638
662
|
"""
|
639
663
|
self.constituents += [new_series]
|
664
|
+
# noinspection PyUnreachableCode
|
640
665
|
self.tsdf = concat([self.tsdf, new_series.tsdf], axis="columns", sort=True)
|
641
666
|
return self
|
642
667
|
|
@@ -841,6 +866,7 @@ class OpenFrame(_CommonModel):
|
|
841
866
|
:,
|
842
867
|
item,
|
843
868
|
]
|
869
|
+
# noinspection PyTypeChecker
|
844
870
|
relative = 1.0 + longdf - shortdf
|
845
871
|
vol = float(
|
846
872
|
relative.pct_change(fill_method=cast(str, None)).std()
|
@@ -935,6 +961,7 @@ class OpenFrame(_CommonModel):
|
|
935
961
|
:,
|
936
962
|
item,
|
937
963
|
]
|
964
|
+
# noinspection PyTypeChecker
|
938
965
|
relative = 1.0 + longdf - shortdf
|
939
966
|
ret = float(
|
940
967
|
relative.pct_change(fill_method=cast(str, None)).mean()
|
@@ -1445,15 +1472,6 @@ class OpenFrame(_CommonModel):
|
|
1445
1472
|
self: Self,
|
1446
1473
|
name: str,
|
1447
1474
|
weight_strat: Optional[LiteralPortfolioWeightings] = None,
|
1448
|
-
initial_weights: Optional[list[float]] = None,
|
1449
|
-
risk_weights: Optional[list[float]] = None,
|
1450
|
-
risk_parity_method: LiteralRiskParityMethod = "ccd",
|
1451
|
-
maximum_iterations: int = 100,
|
1452
|
-
tolerance: float = 1e-8,
|
1453
|
-
weight_bounds: tuple[float, float] = (0.0, 1.0),
|
1454
|
-
riskfree: float = 0.0,
|
1455
|
-
covar_method: LiteralCovMethod = "ledoit-wolf",
|
1456
|
-
options: Optional[dict[str, int]] = None,
|
1457
1475
|
) -> DataFrame:
|
1458
1476
|
"""
|
1459
1477
|
Calculate a basket timeseries based on the supplied weights.
|
@@ -1463,25 +1481,7 @@ class OpenFrame(_CommonModel):
|
|
1463
1481
|
name: str
|
1464
1482
|
Name of the basket timeseries
|
1465
1483
|
weight_strat: LiteralPortfolioWeightings, optional
|
1466
|
-
weight calculation
|
1467
|
-
initial_weights: list[float], optional
|
1468
|
-
Starting asset weights, default inverse volatility
|
1469
|
-
risk_weights: list[float], optional
|
1470
|
-
Risk target weights, default equal weight
|
1471
|
-
risk_parity_method: LiteralRiskParityMethod, default: ccd
|
1472
|
-
Risk parity estimation method
|
1473
|
-
maximum_iterations: int, default: 100
|
1474
|
-
Maximum iterations in iterative solutions
|
1475
|
-
tolerance: float, default: 1e-8
|
1476
|
-
Tolerance level in iterative solutions
|
1477
|
-
weight_bounds: tuple[float, float], default: (0.0, 1.0)
|
1478
|
-
Weigh limits for optimization
|
1479
|
-
riskfree: float, default: 0.0
|
1480
|
-
Risk-free rate used in utility calculation
|
1481
|
-
covar_method: LiteralCovMethod, default: ledoit-wolf
|
1482
|
-
Covariance matrix estimation method
|
1483
|
-
options: dict, optional
|
1484
|
-
options for minimizing, e.g. {'maxiter': 10000 }
|
1484
|
+
weight calculation strategies
|
1485
1485
|
|
1486
1486
|
Returns
|
1487
1487
|
-------
|
@@ -1507,32 +1507,9 @@ class OpenFrame(_CommonModel):
|
|
1507
1507
|
if weight_strat:
|
1508
1508
|
if weight_strat == "eq_weights":
|
1509
1509
|
self.weights = [1.0 / self.item_count] * self.item_count
|
1510
|
-
elif weight_strat == "eq_risk":
|
1511
|
-
weight_calc = list(
|
1512
|
-
calc_erc_weights(
|
1513
|
-
returns=dframe,
|
1514
|
-
initial_weights=initial_weights,
|
1515
|
-
risk_weights=risk_weights,
|
1516
|
-
risk_parity_method=risk_parity_method,
|
1517
|
-
maximum_iterations=maximum_iterations,
|
1518
|
-
tolerance=tolerance,
|
1519
|
-
),
|
1520
|
-
)
|
1521
|
-
self.weights = weight_calc
|
1522
1510
|
elif weight_strat == "inv_vol":
|
1523
1511
|
weight_calc = list(_calc_inv_vol_weights(returns=dframe))
|
1524
1512
|
self.weights = weight_calc
|
1525
|
-
elif weight_strat == "mean_var":
|
1526
|
-
weight_calc = list(
|
1527
|
-
calc_mean_var_weights(
|
1528
|
-
returns=dframe,
|
1529
|
-
weight_bounds=weight_bounds,
|
1530
|
-
rf=riskfree,
|
1531
|
-
covar_method=covar_method,
|
1532
|
-
options=options,
|
1533
|
-
),
|
1534
|
-
)
|
1535
|
-
self.weights = weight_calc
|
1536
1513
|
else:
|
1537
1514
|
msg = "Weight strategy not implemented"
|
1538
1515
|
raise NotImplementedError(msg)
|
@@ -1718,3 +1695,546 @@ class OpenFrame(_CommonModel):
|
|
1718
1695
|
)
|
1719
1696
|
|
1720
1697
|
return DataFrame(corrdf)
|
1698
|
+
|
1699
|
+
|
1700
|
+
def simulate_portfolios(
|
1701
|
+
simframe: OpenFrame,
|
1702
|
+
num_ports: int,
|
1703
|
+
seed: int,
|
1704
|
+
) -> DataFrame:
|
1705
|
+
"""
|
1706
|
+
Generate random weights for simulated portfolios.
|
1707
|
+
|
1708
|
+
Parameters
|
1709
|
+
----------
|
1710
|
+
simframe: OpenFrame
|
1711
|
+
Return data for portfolio constituents
|
1712
|
+
num_ports: int
|
1713
|
+
Number of possible portfolios to simulate
|
1714
|
+
seed: int
|
1715
|
+
The seed for the random process
|
1716
|
+
|
1717
|
+
Returns
|
1718
|
+
-------
|
1719
|
+
pandas.DataFrame
|
1720
|
+
The resulting data
|
1721
|
+
|
1722
|
+
"""
|
1723
|
+
copi = simframe.from_deepcopy()
|
1724
|
+
|
1725
|
+
if any(
|
1726
|
+
x == ValueType.PRICE for x in copi.tsdf.columns.get_level_values(1).to_numpy()
|
1727
|
+
):
|
1728
|
+
copi.value_to_ret()
|
1729
|
+
log_ret = copi.tsdf.copy()[1:]
|
1730
|
+
else:
|
1731
|
+
log_ret = copi.tsdf.copy()
|
1732
|
+
|
1733
|
+
log_ret.columns = log_ret.columns.droplevel(level=1)
|
1734
|
+
|
1735
|
+
randomizer = random_generator(seed=seed)
|
1736
|
+
|
1737
|
+
all_weights = zeros((num_ports, simframe.item_count))
|
1738
|
+
ret_arr = zeros(num_ports)
|
1739
|
+
vol_arr = zeros(num_ports)
|
1740
|
+
sharpe_arr = zeros(num_ports)
|
1741
|
+
|
1742
|
+
for x in range(num_ports):
|
1743
|
+
weights = array(randomizer.random(simframe.item_count))
|
1744
|
+
weights = weights / npsum(weights)
|
1745
|
+
all_weights[x, :] = weights
|
1746
|
+
|
1747
|
+
vol_arr[x] = sqrt(
|
1748
|
+
dot(
|
1749
|
+
weights.T,
|
1750
|
+
dot(log_ret.cov() * simframe.periods_in_a_year, weights),
|
1751
|
+
),
|
1752
|
+
)
|
1753
|
+
|
1754
|
+
ret_arr[x] = npsum(log_ret.mean() * weights * simframe.periods_in_a_year)
|
1755
|
+
|
1756
|
+
sharpe_arr[x] = ret_arr[x] / vol_arr[x]
|
1757
|
+
|
1758
|
+
# noinspection PyUnreachableCode
|
1759
|
+
simdf = concat(
|
1760
|
+
[
|
1761
|
+
DataFrame({"stdev": vol_arr, "ret": ret_arr, "sharpe": sharpe_arr}),
|
1762
|
+
DataFrame(all_weights, columns=simframe.columns_lvl_zero),
|
1763
|
+
],
|
1764
|
+
axis="columns",
|
1765
|
+
)
|
1766
|
+
simdf = simdf.replace([inf, -inf], nan)
|
1767
|
+
return simdf.dropna()
|
1768
|
+
|
1769
|
+
|
1770
|
+
def efficient_frontier( # noqa: C901
|
1771
|
+
eframe: OpenFrame,
|
1772
|
+
num_ports: int = 5000,
|
1773
|
+
seed: int = 71,
|
1774
|
+
upperbounds: float = 1.0,
|
1775
|
+
frontier_points: int = 200,
|
1776
|
+
*,
|
1777
|
+
tweak: bool = True,
|
1778
|
+
) -> tuple[DataFrame, DataFrame, NDArray[float64]]:
|
1779
|
+
"""
|
1780
|
+
Identify an efficient frontier.
|
1781
|
+
|
1782
|
+
Parameters
|
1783
|
+
----------
|
1784
|
+
eframe: OpenFrame
|
1785
|
+
Portfolio data
|
1786
|
+
num_ports: int, default: 5000
|
1787
|
+
Number of possible portfolios to simulate
|
1788
|
+
seed: int, default: 71
|
1789
|
+
The seed for the random process
|
1790
|
+
upperbounds: float, default: 1.0
|
1791
|
+
The largest allowed allocation to a single asset
|
1792
|
+
frontier_points: int, default: 200
|
1793
|
+
number of points along frontier to optimize
|
1794
|
+
tweak: bool, default: True
|
1795
|
+
cutting the frontier to exclude multiple points with almost the same risk
|
1796
|
+
|
1797
|
+
Returns
|
1798
|
+
-------
|
1799
|
+
tuple[DataFrame, DataFrame, NDArray[float]]
|
1800
|
+
The efficient frontier data, simulation data and optimal portfolio
|
1801
|
+
|
1802
|
+
"""
|
1803
|
+
if eframe.weights is None:
|
1804
|
+
eframe.weights = [1.0 / eframe.item_count] * eframe.item_count
|
1805
|
+
|
1806
|
+
copi = eframe.from_deepcopy()
|
1807
|
+
|
1808
|
+
if any(
|
1809
|
+
x == ValueType.PRICE for x in copi.tsdf.columns.get_level_values(1).to_numpy()
|
1810
|
+
):
|
1811
|
+
copi.value_to_ret()
|
1812
|
+
log_ret = copi.tsdf.copy()[1:]
|
1813
|
+
else:
|
1814
|
+
log_ret = copi.tsdf.copy()
|
1815
|
+
|
1816
|
+
log_ret.columns = log_ret.columns.droplevel(level=1)
|
1817
|
+
|
1818
|
+
simulated = simulate_portfolios(simframe=copi, num_ports=num_ports, seed=seed)
|
1819
|
+
|
1820
|
+
frontier_min = simulated.loc[simulated["stdev"].idxmin()]["ret"]
|
1821
|
+
arithmetic_mean = log_ret.mean() * copi.periods_in_a_year
|
1822
|
+
frontier_max = 0.0
|
1823
|
+
if isinstance(arithmetic_mean, Series):
|
1824
|
+
frontier_max = arithmetic_mean.max()
|
1825
|
+
|
1826
|
+
def _check_sum(weights: NDArray[float64]) -> float64:
|
1827
|
+
return cast(float64, npsum(weights) - 1)
|
1828
|
+
|
1829
|
+
def _get_ret_vol_sr(
|
1830
|
+
lg_ret: DataFrame,
|
1831
|
+
weights: NDArray[float64],
|
1832
|
+
per_in_yr: float,
|
1833
|
+
) -> NDArray[float64]:
|
1834
|
+
ret = npsum(lg_ret.mean() * weights) * per_in_yr
|
1835
|
+
volatility = sqrt(dot(weights.T, dot(lg_ret.cov() * per_in_yr, weights)))
|
1836
|
+
sr = ret / volatility
|
1837
|
+
return cast(NDArray[float64], array([ret, volatility, sr]))
|
1838
|
+
|
1839
|
+
def _diff_return(
|
1840
|
+
lg_ret: DataFrame,
|
1841
|
+
weights: NDArray[float64],
|
1842
|
+
per_in_yr: float,
|
1843
|
+
poss_return: float,
|
1844
|
+
) -> float64:
|
1845
|
+
return cast(
|
1846
|
+
float64,
|
1847
|
+
_get_ret_vol_sr(lg_ret=lg_ret, weights=weights, per_in_yr=per_in_yr)[0]
|
1848
|
+
- poss_return,
|
1849
|
+
)
|
1850
|
+
|
1851
|
+
def _neg_sharpe(weights: NDArray[float64]) -> float64:
|
1852
|
+
return cast(
|
1853
|
+
float64,
|
1854
|
+
_get_ret_vol_sr(
|
1855
|
+
lg_ret=log_ret,
|
1856
|
+
weights=weights,
|
1857
|
+
per_in_yr=eframe.periods_in_a_year,
|
1858
|
+
)[2]
|
1859
|
+
* -1,
|
1860
|
+
)
|
1861
|
+
|
1862
|
+
def _minimize_volatility(
|
1863
|
+
weights: NDArray[float64],
|
1864
|
+
) -> float64:
|
1865
|
+
return cast(
|
1866
|
+
float64,
|
1867
|
+
_get_ret_vol_sr(
|
1868
|
+
lg_ret=log_ret,
|
1869
|
+
weights=weights,
|
1870
|
+
per_in_yr=eframe.periods_in_a_year,
|
1871
|
+
)[1],
|
1872
|
+
)
|
1873
|
+
|
1874
|
+
constraints = {"type": "eq", "fun": _check_sum}
|
1875
|
+
bounds = tuple((0, upperbounds) for _ in range(eframe.item_count))
|
1876
|
+
init_guess = array(eframe.weights)
|
1877
|
+
|
1878
|
+
opt_results = minimize(
|
1879
|
+
fun=_neg_sharpe,
|
1880
|
+
x0=init_guess,
|
1881
|
+
method="SLSQP",
|
1882
|
+
bounds=bounds,
|
1883
|
+
constraints=constraints,
|
1884
|
+
)
|
1885
|
+
|
1886
|
+
optimal = _get_ret_vol_sr(
|
1887
|
+
lg_ret=log_ret,
|
1888
|
+
weights=opt_results.x,
|
1889
|
+
per_in_yr=eframe.periods_in_a_year,
|
1890
|
+
)
|
1891
|
+
|
1892
|
+
frontier_y = linspace(start=frontier_min, stop=frontier_max, num=frontier_points)
|
1893
|
+
frontier_x = []
|
1894
|
+
frontier_weights = []
|
1895
|
+
|
1896
|
+
for possible_return in frontier_y:
|
1897
|
+
cons = cast(
|
1898
|
+
dict[str, Union[str, Callable[[float, NDArray[float64]], float64]]],
|
1899
|
+
(
|
1900
|
+
{"type": "eq", "fun": _check_sum},
|
1901
|
+
{
|
1902
|
+
"type": "eq",
|
1903
|
+
"fun": lambda w, poss_return=possible_return: _diff_return(
|
1904
|
+
lg_ret=log_ret,
|
1905
|
+
weights=w,
|
1906
|
+
per_in_yr=eframe.periods_in_a_year,
|
1907
|
+
poss_return=poss_return,
|
1908
|
+
),
|
1909
|
+
},
|
1910
|
+
),
|
1911
|
+
)
|
1912
|
+
|
1913
|
+
result = minimize(
|
1914
|
+
fun=_minimize_volatility,
|
1915
|
+
x0=init_guess,
|
1916
|
+
method="SLSQP",
|
1917
|
+
bounds=bounds,
|
1918
|
+
constraints=cons,
|
1919
|
+
)
|
1920
|
+
|
1921
|
+
frontier_x.append(result["fun"])
|
1922
|
+
frontier_weights.append(result["x"])
|
1923
|
+
|
1924
|
+
# noinspection PyUnreachableCode
|
1925
|
+
line_df = concat(
|
1926
|
+
[
|
1927
|
+
DataFrame(data=frontier_weights, columns=eframe.columns_lvl_zero),
|
1928
|
+
DataFrame({"stdev": frontier_x, "ret": frontier_y}),
|
1929
|
+
],
|
1930
|
+
axis="columns",
|
1931
|
+
)
|
1932
|
+
line_df["sharpe"] = line_df.ret / line_df.stdev
|
1933
|
+
|
1934
|
+
limit_small = 0.0001
|
1935
|
+
line_df = line_df.mask(line_df.abs() < limit_small, 0.0)
|
1936
|
+
line_df["text"] = line_df.apply(
|
1937
|
+
lambda c: "<br>".join(
|
1938
|
+
[f"{c[nm]:.1%} - {nm}" for nm in eframe.columns_lvl_zero],
|
1939
|
+
),
|
1940
|
+
axis="columns",
|
1941
|
+
)
|
1942
|
+
|
1943
|
+
if tweak:
|
1944
|
+
limit_tweak = 0.001
|
1945
|
+
line_df["stdev_diff"] = line_df.stdev.pct_change()
|
1946
|
+
line_df = line_df.loc[line_df.stdev_diff.abs() > limit_tweak]
|
1947
|
+
line_df = line_df.drop(columns="stdev_diff")
|
1948
|
+
|
1949
|
+
return line_df, simulated, append(optimal, opt_results.x)
|
1950
|
+
|
1951
|
+
|
1952
|
+
def constrain_optimized_portfolios(
|
1953
|
+
data: OpenFrame,
|
1954
|
+
serie: OpenTimeSeries,
|
1955
|
+
portfolioname: str = "Current Portfolio",
|
1956
|
+
simulations: int = 10000,
|
1957
|
+
curve_points: int = 200,
|
1958
|
+
upper_bound: float = 0.25,
|
1959
|
+
) -> tuple[OpenFrame, OpenTimeSeries, OpenFrame, OpenTimeSeries]:
|
1960
|
+
"""
|
1961
|
+
Constrain optimized portfolios to those that improve on the current one.
|
1962
|
+
|
1963
|
+
Parameters
|
1964
|
+
----------
|
1965
|
+
data: OpenFrame
|
1966
|
+
Portfolio data
|
1967
|
+
serie: OpenTimeSeries
|
1968
|
+
A
|
1969
|
+
portfolioname: str, default: "Current Portfolio"
|
1970
|
+
Name of the portfolio
|
1971
|
+
simulations: int, default: 10000
|
1972
|
+
Number of possible portfolios to simulate
|
1973
|
+
curve_points: int, default: 200
|
1974
|
+
Number of optimal portfolios on the efficient frontier
|
1975
|
+
upper_bound: float, default: 0.25
|
1976
|
+
The largest allowed allocation to a single asset
|
1977
|
+
|
1978
|
+
Returns
|
1979
|
+
-------
|
1980
|
+
tuple[OpenFrame, OpenTimeSeries, OpenFrame, OpenTimeSeries]
|
1981
|
+
The constrained optimal portfolio data
|
1982
|
+
|
1983
|
+
"""
|
1984
|
+
lr_frame = data.from_deepcopy()
|
1985
|
+
mv_frame = data.from_deepcopy()
|
1986
|
+
|
1987
|
+
front_frame, sim_frame, optimal = efficient_frontier(
|
1988
|
+
eframe=data,
|
1989
|
+
num_ports=simulations,
|
1990
|
+
frontier_points=curve_points,
|
1991
|
+
upperbounds=upper_bound,
|
1992
|
+
)
|
1993
|
+
|
1994
|
+
condition_least_ret = front_frame.ret > serie.arithmetic_ret
|
1995
|
+
# noinspection PyArgumentList
|
1996
|
+
least_ret_frame = front_frame[condition_least_ret].sort_values(by="stdev")
|
1997
|
+
least_ret_port = least_ret_frame.iloc[0]
|
1998
|
+
least_ret_port_name = f"Minimize vol & target return of {portfolioname}"
|
1999
|
+
least_ret_weights = [least_ret_port[c] for c in lr_frame.columns_lvl_zero]
|
2000
|
+
lr_frame.weights = least_ret_weights
|
2001
|
+
resleast = OpenTimeSeries.from_df(lr_frame.make_portfolio(least_ret_port_name))
|
2002
|
+
|
2003
|
+
condition_most_vol = front_frame.stdev < serie.vol
|
2004
|
+
# noinspection PyArgumentList
|
2005
|
+
most_vol_frame = front_frame[condition_most_vol].sort_values(
|
2006
|
+
by="ret",
|
2007
|
+
ascending=False,
|
2008
|
+
)
|
2009
|
+
most_vol_port = most_vol_frame.iloc[0]
|
2010
|
+
most_vol_port_name = f"Maximize return & target risk of {portfolioname}"
|
2011
|
+
most_vol_weights = [most_vol_port[c] for c in mv_frame.columns_lvl_zero]
|
2012
|
+
mv_frame.weights = most_vol_weights
|
2013
|
+
resmost = OpenTimeSeries.from_df(mv_frame.make_portfolio(most_vol_port_name))
|
2014
|
+
|
2015
|
+
return lr_frame, resleast, mv_frame, resmost
|
2016
|
+
|
2017
|
+
|
2018
|
+
def prepare_plot_data(
|
2019
|
+
assets: OpenFrame,
|
2020
|
+
current: OpenTimeSeries,
|
2021
|
+
optimized: NDArray[float64],
|
2022
|
+
) -> DataFrame:
|
2023
|
+
"""
|
2024
|
+
Prepare date to be used as point_frame in the sharpeplot function.
|
2025
|
+
|
2026
|
+
Parameters
|
2027
|
+
----------
|
2028
|
+
assets: OpenFrame
|
2029
|
+
Portfolio data with individual assets and a weighted portfolio
|
2030
|
+
current: OpenTimeSeries
|
2031
|
+
The current or initial portfolio based on given weights
|
2032
|
+
optimized: DataFrame
|
2033
|
+
Data optimized with the efficient_frontier method
|
2034
|
+
|
2035
|
+
Returns
|
2036
|
+
-------
|
2037
|
+
DataFrame
|
2038
|
+
The data prepared with mean returns, volatility and weights
|
2039
|
+
|
2040
|
+
"""
|
2041
|
+
txt = "<br>".join(
|
2042
|
+
[
|
2043
|
+
f"{wgt:.1%} - {nm}"
|
2044
|
+
for wgt, nm in zip(
|
2045
|
+
cast(list[float], assets.weights),
|
2046
|
+
assets.columns_lvl_zero,
|
2047
|
+
)
|
2048
|
+
],
|
2049
|
+
)
|
2050
|
+
|
2051
|
+
opt_text = "<br>".join(
|
2052
|
+
[
|
2053
|
+
f"{wgt:.1%} - {nm}"
|
2054
|
+
for wgt, nm in zip(optimized[3:], assets.columns_lvl_zero)
|
2055
|
+
],
|
2056
|
+
)
|
2057
|
+
vol: Series[float] = assets.vol
|
2058
|
+
plotframe = DataFrame(
|
2059
|
+
data=[
|
2060
|
+
assets.arithmetic_ret,
|
2061
|
+
vol,
|
2062
|
+
Series(
|
2063
|
+
data=[""] * assets.item_count,
|
2064
|
+
index=vol.index,
|
2065
|
+
),
|
2066
|
+
],
|
2067
|
+
index=["ret", "stdev", "text"],
|
2068
|
+
)
|
2069
|
+
plotframe.columns = plotframe.columns.droplevel(level=1)
|
2070
|
+
plotframe["Max Sharpe Portfolio"] = [optimized[0], optimized[1], opt_text]
|
2071
|
+
plotframe[current.label] = [current.arithmetic_ret, current.vol, txt]
|
2072
|
+
|
2073
|
+
return plotframe
|
2074
|
+
|
2075
|
+
|
2076
|
+
def sharpeplot( # noqa: C901
|
2077
|
+
sim_frame: DataFrame = None,
|
2078
|
+
line_frame: DataFrame = None,
|
2079
|
+
point_frame: DataFrame = None,
|
2080
|
+
point_frame_mode: LiteralLinePlotMode = "markers",
|
2081
|
+
filename: Optional[str] = None,
|
2082
|
+
directory: Optional[DirectoryPath] = None,
|
2083
|
+
titletext: Optional[str] = None,
|
2084
|
+
output_type: LiteralPlotlyOutput = "file",
|
2085
|
+
include_plotlyjs: LiteralPlotlyJSlib = "cdn",
|
2086
|
+
*,
|
2087
|
+
title: bool = True,
|
2088
|
+
add_logo: bool = True,
|
2089
|
+
auto_open: bool = True,
|
2090
|
+
) -> tuple[Figure, str]:
|
2091
|
+
"""
|
2092
|
+
Create scatter plot coloured by Sharpe Ratio.
|
2093
|
+
|
2094
|
+
Parameters
|
2095
|
+
----------
|
2096
|
+
sim_frame: DataFrame, optional
|
2097
|
+
Data from the simulate_portfolios method.
|
2098
|
+
line_frame: DataFrame, optional
|
2099
|
+
Data from the efficient_frontier method.
|
2100
|
+
point_frame: DataFrame, optional
|
2101
|
+
Data to highlight current and efficient portfolios.
|
2102
|
+
point_frame_mode: LiteralLinePlotMode, default: markers
|
2103
|
+
Which type of scatter to use.
|
2104
|
+
filename: str, optional
|
2105
|
+
Name of the Plotly html file
|
2106
|
+
directory: DirectoryPath, optional
|
2107
|
+
Directory where Plotly html file is saved
|
2108
|
+
titletext: str, optional
|
2109
|
+
Text for the plot title
|
2110
|
+
output_type: LiteralPlotlyOutput, default: "file"
|
2111
|
+
Determines output type
|
2112
|
+
include_plotlyjs: LiteralPlotlyJSlib, default: "cdn"
|
2113
|
+
Determines how the plotly.js library is included in the output
|
2114
|
+
title: bool, default: True
|
2115
|
+
Whether to add standard plot title
|
2116
|
+
add_logo: bool, default: True
|
2117
|
+
Whether to add Captor logo
|
2118
|
+
auto_open: bool, default: True
|
2119
|
+
Determines whether to open a browser window with the plot
|
2120
|
+
|
2121
|
+
Returns
|
2122
|
+
-------
|
2123
|
+
Figure
|
2124
|
+
The scatter plot with simulated and optimized results
|
2125
|
+
|
2126
|
+
"""
|
2127
|
+
returns = []
|
2128
|
+
risk = []
|
2129
|
+
|
2130
|
+
if directory:
|
2131
|
+
dirpath = Path(directory).resolve()
|
2132
|
+
elif Path.home().joinpath("Documents").exists():
|
2133
|
+
dirpath = Path.home().joinpath("Documents")
|
2134
|
+
else:
|
2135
|
+
dirpath = Path(stack()[1].filename).parent
|
2136
|
+
|
2137
|
+
if not filename:
|
2138
|
+
filename = "sharpeplot.html"
|
2139
|
+
plotfile = dirpath.joinpath(filename)
|
2140
|
+
|
2141
|
+
fig, logo = load_plotly_dict()
|
2142
|
+
figure = Figure(fig)
|
2143
|
+
|
2144
|
+
if sim_frame is not None:
|
2145
|
+
returns.extend(list(sim_frame.loc[:, "ret"]))
|
2146
|
+
risk.extend(list(sim_frame.loc[:, "stdev"]))
|
2147
|
+
figure.add_scatter(
|
2148
|
+
x=sim_frame.loc[:, "stdev"],
|
2149
|
+
y=sim_frame.loc[:, "ret"],
|
2150
|
+
hoverinfo="skip",
|
2151
|
+
marker={
|
2152
|
+
"size": 10,
|
2153
|
+
"opacity": 0.5,
|
2154
|
+
"color": sim_frame.loc[:, "sharpe"],
|
2155
|
+
"colorscale": "Jet",
|
2156
|
+
"reversescale": True,
|
2157
|
+
"colorbar": {"thickness": 20, "title": "Ratio<br>ret / vol"},
|
2158
|
+
},
|
2159
|
+
mode="markers",
|
2160
|
+
name="simulated portfolios",
|
2161
|
+
)
|
2162
|
+
if line_frame is not None:
|
2163
|
+
returns.extend(list(line_frame.loc[:, "ret"]))
|
2164
|
+
risk.extend(list(line_frame.loc[:, "stdev"]))
|
2165
|
+
figure.add_scatter(
|
2166
|
+
x=line_frame.loc[:, "stdev"],
|
2167
|
+
y=line_frame.loc[:, "ret"],
|
2168
|
+
text=line_frame.loc[:, "text"],
|
2169
|
+
hovertemplate="%{text}<br>Return %{y}<br>Vol %{x}",
|
2170
|
+
line={"width": 2.5, "dash": "solid"},
|
2171
|
+
mode="lines",
|
2172
|
+
name="Efficient frontier",
|
2173
|
+
)
|
2174
|
+
|
2175
|
+
colorway = cast(dict[str, list[str]], fig["layout"]).get("colorway")[
|
2176
|
+
: len(point_frame.columns)
|
2177
|
+
]
|
2178
|
+
|
2179
|
+
if point_frame is not None:
|
2180
|
+
for col, clr in zip(point_frame.columns, colorway):
|
2181
|
+
returns.extend([point_frame.loc["ret", col]])
|
2182
|
+
risk.extend([point_frame.loc["stdev", col]])
|
2183
|
+
figure.add_scatter(
|
2184
|
+
x=[point_frame.loc["stdev", col]],
|
2185
|
+
xhoverformat=".2%",
|
2186
|
+
y=[point_frame.loc["ret", col]],
|
2187
|
+
yhoverformat=".2%",
|
2188
|
+
hovertext=[point_frame.loc["text", col]],
|
2189
|
+
hoverinfo="x+y+text+name",
|
2190
|
+
marker={"size": 20, "color": clr},
|
2191
|
+
mode=point_frame_mode,
|
2192
|
+
name=col,
|
2193
|
+
text=col,
|
2194
|
+
textfont={"size": 14},
|
2195
|
+
textposition="bottom center",
|
2196
|
+
)
|
2197
|
+
|
2198
|
+
figure.update_layout(
|
2199
|
+
xaxis={"tickformat": ".1%"},
|
2200
|
+
xaxis_title="volatility",
|
2201
|
+
yaxis={
|
2202
|
+
"tickformat": ".1%",
|
2203
|
+
"scaleanchor": "x",
|
2204
|
+
"scaleratio": 1,
|
2205
|
+
},
|
2206
|
+
yaxis_title="annual return",
|
2207
|
+
showlegend=False,
|
2208
|
+
)
|
2209
|
+
if title:
|
2210
|
+
if titletext is None:
|
2211
|
+
titletext = "<b>Risk and Return</b><br>"
|
2212
|
+
figure.update_layout(title={"text": titletext, "font": {"size": 32}})
|
2213
|
+
|
2214
|
+
if add_logo:
|
2215
|
+
figure.add_layout_image(logo)
|
2216
|
+
|
2217
|
+
if output_type == "file":
|
2218
|
+
plot(
|
2219
|
+
figure_or_data=figure,
|
2220
|
+
filename=str(plotfile),
|
2221
|
+
auto_open=auto_open,
|
2222
|
+
auto_play=False,
|
2223
|
+
link_text="",
|
2224
|
+
include_plotlyjs=cast(bool, include_plotlyjs),
|
2225
|
+
config=fig["config"],
|
2226
|
+
output_type=output_type,
|
2227
|
+
)
|
2228
|
+
string_output = str(plotfile)
|
2229
|
+
else:
|
2230
|
+
div_id = filename.split(sep=".")[0]
|
2231
|
+
string_output = to_html(
|
2232
|
+
fig=figure,
|
2233
|
+
config=fig["config"],
|
2234
|
+
auto_play=False,
|
2235
|
+
include_plotlyjs=cast(bool, include_plotlyjs),
|
2236
|
+
full_html=False,
|
2237
|
+
div_id=div_id,
|
2238
|
+
)
|
2239
|
+
|
2240
|
+
return figure, string_output
|
@@ -1,4 +1,5 @@
|
|
1
1
|
"""Defining the ReturnSimulation class."""
|
2
|
+
|
2
3
|
from __future__ import annotations
|
3
4
|
|
4
5
|
import datetime as dt
|
@@ -6,7 +7,12 @@ from typing import Optional, cast
|
|
6
7
|
|
7
8
|
from numpy import multiply, sqrt
|
8
9
|
from numpy.random import PCG64, Generator, SeedSequence
|
9
|
-
from pandas import
|
10
|
+
from pandas import (
|
11
|
+
DataFrame,
|
12
|
+
Index,
|
13
|
+
MultiIndex,
|
14
|
+
concat,
|
15
|
+
)
|
10
16
|
from pydantic import (
|
11
17
|
BaseModel,
|
12
18
|
ConfigDict,
|
@@ -428,7 +434,7 @@ class ReturnSimulation(BaseModel):
|
|
428
434
|
Returns
|
429
435
|
-------
|
430
436
|
pandas.DataFrame
|
431
|
-
|
437
|
+
The simulation(s) data
|
432
438
|
|
433
439
|
"""
|
434
440
|
d_range = generate_calendar_date_range(
|
@@ -138,8 +138,6 @@ LiteralPlotlyOutput = Literal["file", "div"]
|
|
138
138
|
LiteralPlotlyJSlib = Literal[True, False, "cdn"]
|
139
139
|
LiteralOlsFitMethod = Literal["pinv", "qr"]
|
140
140
|
LiteralPortfolioWeightings = Literal["eq_weights", "eq_risk", "inv_vol", "mean_var"]
|
141
|
-
LiteralCovMethod = Literal["ledoit-wolf", "standard"]
|
142
|
-
LiteralRiskParityMethod = Literal["ccd", "slsqp"]
|
143
141
|
LiteralOlsFitCovType = Literal[
|
144
142
|
"nonrobust",
|
145
143
|
"fixed scale",
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "openseries"
|
3
|
-
version = "1.
|
3
|
+
version = "1.5.1"
|
4
4
|
description = "Package for analyzing financial timeseries."
|
5
5
|
authors = ["Martin Karrin <martin.karrin@captor.se>"]
|
6
6
|
repository = "https://github.com/CaptorAB/OpenSeries"
|
@@ -34,7 +34,6 @@ keywords = [
|
|
34
34
|
|
35
35
|
[tool.poetry.dependencies]
|
36
36
|
python = ">=3.9,<3.13"
|
37
|
-
ffn = ">=1.0.0,<2.0.0"
|
38
37
|
holidays = ">=0.30,<1.0"
|
39
38
|
numpy = ">=1.23.2,<=2.0.0"
|
40
39
|
openpyxl = ">=3.1.2,<4.0.0"
|
@@ -43,37 +42,36 @@ plotly = ">=5.18.0,<6.0.0"
|
|
43
42
|
pyarrow = ">=14.0.2,<16.0.0"
|
44
43
|
pydantic = ">=2.5.2,<3.0.0"
|
45
44
|
python-dateutil = ">=2.8.2,<3.0.0"
|
45
|
+
requests = ">=2.20.0,<3.0.0"
|
46
46
|
scipy = ">=1.11.4,<2.0.0"
|
47
47
|
statsmodels = ">=0.14.0,<1.0.0"
|
48
48
|
|
49
49
|
[tool.poetry.group.dev.dependencies]
|
50
|
-
coverage = "^7.4.
|
50
|
+
coverage = "^7.4.4"
|
51
51
|
coverage-badge = "^1.1.0"
|
52
|
-
mypy = "^1.
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
types-openpyxl = "^3.1.0.
|
58
|
-
|
59
|
-
types-
|
60
|
-
types-requests = "^2.31.0.20240218"
|
61
|
-
types-toml = "^0.10.8.7"
|
52
|
+
mypy = "^1.9.0"
|
53
|
+
pandas-stubs = "^2.2.1.240316"
|
54
|
+
pre-commit = "^3.7.0"
|
55
|
+
pytest = "^8.1.1"
|
56
|
+
ruff = "^0.3.4"
|
57
|
+
types-openpyxl = "^3.1.0.20240311"
|
58
|
+
types-python-dateutil = "^2.9.0.20240316"
|
59
|
+
types-requests = "^2.31.0.20240311"
|
62
60
|
|
63
61
|
[build-system]
|
64
|
-
requires = ["poetry-core"]
|
62
|
+
requires = ["poetry-core>=1.8.2"]
|
65
63
|
build-backend = "poetry.core.masonry.api"
|
66
64
|
|
67
65
|
[tool.setuptools_scm]
|
68
66
|
|
69
67
|
[poetry.virtualenvs]
|
68
|
+
create = true
|
70
69
|
in-project = true
|
71
70
|
path = "venv"
|
72
71
|
|
73
72
|
[tool.pytest.ini_options]
|
74
73
|
filterwarnings = [
|
75
|
-
|
76
|
-
"ignore::DeprecationWarning:pandas_datareader.*:"
|
74
|
+
"ignore::RuntimeWarning:pandas.*:"
|
77
75
|
]
|
78
76
|
|
79
77
|
[tool.coverage.run]
|
@@ -115,7 +113,7 @@ fixable = ["ALL"]
|
|
115
113
|
[tool.ruff.lint.pylint]
|
116
114
|
max-args = 12
|
117
115
|
max-branches = 22
|
118
|
-
max-statements =
|
116
|
+
max-statements = 66
|
119
117
|
|
120
118
|
[tool.ruff.lint.pyupgrade]
|
121
119
|
keep-runtime-typing = true
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|