openseries 1.8.4__py3-none-any.whl → 1.9.1__py3-none-any.whl
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/__init__.py +10 -1
- openseries/_common_model.py +8 -1
- openseries/_risk.py +8 -1
- openseries/datefixer.py +14 -8
- openseries/frame.py +8 -1
- openseries/load_plotly.py +8 -1
- openseries/owntypes.py +8 -1
- openseries/portfoliotools.py +8 -1
- openseries/report.py +479 -0
- openseries/series.py +8 -1
- openseries/simulation.py +8 -1
- {openseries-1.8.4.dist-info → openseries-1.9.1.dist-info}/METADATA +14 -41
- openseries-1.9.1.dist-info/RECORD +17 -0
- openseries-1.8.4.dist-info/RECORD +0 -16
- {openseries-1.8.4.dist-info → openseries-1.9.1.dist-info}/LICENSE.md +0 -0
- {openseries-1.8.4.dist-info → openseries-1.9.1.dist-info}/WHEEL +0 -0
openseries/__init__.py
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
-
"""openseries.openseries.__init__.py.
|
1
|
+
"""openseries.openseries.__init__.py.
|
2
|
+
|
3
|
+
Copyright (c) Captor Fund Management AB. This file is part of the openseries project.
|
4
|
+
|
5
|
+
Licensed under the BSD 3-Clause License. You may obtain a copy of the License at:
|
6
|
+
https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
|
7
|
+
SPDX-License-Identifier: BSD-3-Clause
|
8
|
+
"""
|
2
9
|
|
3
10
|
from .datefixer import (
|
4
11
|
date_fix,
|
@@ -18,6 +25,7 @@ from .portfoliotools import (
|
|
18
25
|
sharpeplot,
|
19
26
|
simulate_portfolios,
|
20
27
|
)
|
28
|
+
from .report import report_html
|
21
29
|
from .series import OpenTimeSeries, timeseries_chain
|
22
30
|
from .simulation import ReturnSimulation
|
23
31
|
|
@@ -37,6 +45,7 @@ __all__ = [
|
|
37
45
|
"load_plotly_dict",
|
38
46
|
"offset_business_days",
|
39
47
|
"prepare_plot_data",
|
48
|
+
"report_html",
|
40
49
|
"sharpeplot",
|
41
50
|
"simulate_portfolios",
|
42
51
|
"timeseries_chain",
|
openseries/_common_model.py
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
-
"""Defining the _CommonModel class.
|
1
|
+
"""Defining the _CommonModel class.
|
2
|
+
|
3
|
+
Copyright (c) Captor Fund Management AB. This file is part of the openseries project.
|
4
|
+
|
5
|
+
Licensed under the BSD 3-Clause License. You may obtain a copy of the License at:
|
6
|
+
https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
|
7
|
+
SPDX-License-Identifier: BSD-3-Clause
|
8
|
+
"""
|
2
9
|
|
3
10
|
# mypy: disable-error-code="no-any-return"
|
4
11
|
from __future__ import annotations
|
openseries/_risk.py
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
-
"""Various risk related functions.
|
1
|
+
"""Various risk related functions.
|
2
|
+
|
3
|
+
Copyright (c) Captor Fund Management AB. This file is part of the openseries project.
|
4
|
+
|
5
|
+
Licensed under the BSD 3-Clause License. You may obtain a copy of the License at:
|
6
|
+
https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
|
7
|
+
SPDX-License-Identifier: BSD-3-Clause
|
8
|
+
"""
|
2
9
|
|
3
10
|
from __future__ import annotations
|
4
11
|
|
openseries/datefixer.py
CHANGED
@@ -1,11 +1,18 @@
|
|
1
|
-
"""Date related utilities.
|
1
|
+
"""Date related utilities.
|
2
|
+
|
3
|
+
Copyright (c) Captor Fund Management AB. This file is part of the openseries project.
|
4
|
+
|
5
|
+
Licensed under the BSD 3-Clause License. You may obtain a copy of the License at:
|
6
|
+
https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
|
7
|
+
SPDX-License-Identifier: BSD-3-Clause
|
8
|
+
"""
|
2
9
|
|
3
10
|
from __future__ import annotations
|
4
11
|
|
5
12
|
import datetime as dt
|
6
13
|
from typing import TYPE_CHECKING, cast
|
7
14
|
|
8
|
-
import
|
15
|
+
import exchange_calendars as exchcal
|
9
16
|
from dateutil.relativedelta import relativedelta
|
10
17
|
from holidays import (
|
11
18
|
country_holidays,
|
@@ -69,7 +76,7 @@ def market_holidays(
|
|
69
76
|
"""
|
70
77
|
market_list = [markets] if isinstance(markets, str) else list(markets)
|
71
78
|
|
72
|
-
supported =
|
79
|
+
supported = exchcal.get_calendar_names()
|
73
80
|
|
74
81
|
if not all(m in supported for m in market_list):
|
75
82
|
msg = (
|
@@ -80,13 +87,12 @@ def market_holidays(
|
|
80
87
|
|
81
88
|
holidays: list[str] = []
|
82
89
|
for m in market_list:
|
83
|
-
cal =
|
84
|
-
|
85
|
-
cal_hols = cal.holidays().calendar.holidays
|
90
|
+
cal = exchcal.get_calendar(m)
|
91
|
+
cal_hols = cal.regular_holidays.holidays()
|
86
92
|
my_hols: list[str] = [
|
87
|
-
|
93
|
+
date.date().strftime("%Y-%m-%d")
|
88
94
|
for date in cal_hols
|
89
|
-
if (startyear <=
|
95
|
+
if (startyear <= date.date().year <= endyear)
|
90
96
|
]
|
91
97
|
holidays.extend(my_hols)
|
92
98
|
|
openseries/frame.py
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
-
"""Defining the OpenFrame class.
|
1
|
+
"""Defining the OpenFrame class.
|
2
|
+
|
3
|
+
Copyright (c) Captor Fund Management AB. This file is part of the openseries project.
|
4
|
+
|
5
|
+
Licensed under the BSD 3-Clause License. You may obtain a copy of the License at:
|
6
|
+
https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
|
7
|
+
SPDX-License-Identifier: BSD-3-Clause
|
8
|
+
"""
|
2
9
|
|
3
10
|
# mypy: disable-error-code="index,assignment,arg-type,no-any-return"
|
4
11
|
from __future__ import annotations
|
openseries/load_plotly.py
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
-
"""Function to load plotly layout and configuration from local json file.
|
1
|
+
"""Function to load plotly layout and configuration from local json file.
|
2
|
+
|
3
|
+
Copyright (c) Captor Fund Management AB. This file is part of the openseries project.
|
4
|
+
|
5
|
+
Licensed under the BSD 3-Clause License. You may obtain a copy of the License at:
|
6
|
+
https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
|
7
|
+
SPDX-License-Identifier: BSD-3-Clause
|
8
|
+
"""
|
2
9
|
|
3
10
|
from __future__ import annotations
|
4
11
|
|
openseries/owntypes.py
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
-
"""Declaring types used throughout the project.
|
1
|
+
"""Declaring types used throughout the project.
|
2
|
+
|
3
|
+
Copyright (c) Captor Fund Management AB. This file is part of the openseries project.
|
4
|
+
|
5
|
+
Licensed under the BSD 3-Clause License. You may obtain a copy of the License at:
|
6
|
+
https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
|
7
|
+
SPDX-License-Identifier: BSD-3-Clause
|
8
|
+
"""
|
2
9
|
|
3
10
|
from __future__ import annotations
|
4
11
|
|
openseries/portfoliotools.py
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
-
"""Defining the portfolio tools for the OpenFrame class.
|
1
|
+
"""Defining the portfolio tools for the OpenFrame class.
|
2
|
+
|
3
|
+
Copyright (c) Captor Fund Management AB. This file is part of the openseries project.
|
4
|
+
|
5
|
+
Licensed under the BSD 3-Clause License. You may obtain a copy of the License at:
|
6
|
+
https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
|
7
|
+
SPDX-License-Identifier: BSD-3-Clause
|
8
|
+
"""
|
2
9
|
|
3
10
|
# mypy: disable-error-code="index,assignment"
|
4
11
|
from __future__ import annotations
|
openseries/report.py
ADDED
@@ -0,0 +1,479 @@
|
|
1
|
+
"""Functions related to HTML reports.
|
2
|
+
|
3
|
+
Copyright (c) Captor Fund Management AB. This file is part of the openseries project.
|
4
|
+
|
5
|
+
Licensed under the BSD 3-Clause License. You may obtain a copy of the License at:
|
6
|
+
https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
|
7
|
+
SPDX-License-Identifier: BSD-3-Clause
|
8
|
+
"""
|
9
|
+
|
10
|
+
# mypy: disable-error-code="assignment,index,arg-type"
|
11
|
+
from __future__ import annotations
|
12
|
+
|
13
|
+
from inspect import stack
|
14
|
+
from logging import getLogger
|
15
|
+
from pathlib import Path
|
16
|
+
from secrets import choice
|
17
|
+
from string import ascii_letters
|
18
|
+
from typing import TYPE_CHECKING, cast
|
19
|
+
from warnings import catch_warnings, simplefilter
|
20
|
+
|
21
|
+
if TYPE_CHECKING: # pragma: no cover
|
22
|
+
from pandas import Series
|
23
|
+
from plotly.graph_objs import Figure
|
24
|
+
|
25
|
+
from .frame import OpenFrame
|
26
|
+
from .owntypes import LiteralPlotlyJSlib, LiteralPlotlyOutput
|
27
|
+
|
28
|
+
|
29
|
+
from pandas import DataFrame, Series, concat
|
30
|
+
from plotly.io import to_html
|
31
|
+
from plotly.offline import plot
|
32
|
+
from plotly.subplots import make_subplots
|
33
|
+
|
34
|
+
from .load_plotly import load_plotly_dict
|
35
|
+
from .owntypes import (
|
36
|
+
LiteralBizDayFreq,
|
37
|
+
ValueType,
|
38
|
+
)
|
39
|
+
|
40
|
+
logger = getLogger(__name__)
|
41
|
+
|
42
|
+
|
43
|
+
__all__ = ["report_html"]
|
44
|
+
|
45
|
+
|
46
|
+
def calendar_period_returns(
|
47
|
+
data: OpenFrame,
|
48
|
+
freq: LiteralBizDayFreq = "BYE",
|
49
|
+
*,
|
50
|
+
relabel: bool = True,
|
51
|
+
) -> DataFrame:
|
52
|
+
"""Generate a table of returns with appropriate table labels.
|
53
|
+
|
54
|
+
Parameters
|
55
|
+
----------
|
56
|
+
data: OpenFrame
|
57
|
+
The timeseries data
|
58
|
+
freq: LiteralBizDayFreq
|
59
|
+
The date offset string that sets the resampled frequency
|
60
|
+
relabel: bool, default: True
|
61
|
+
Whether to set new appropriate labels
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
-------
|
65
|
+
pandas.DataFrame
|
66
|
+
The resulting data
|
67
|
+
|
68
|
+
"""
|
69
|
+
copied = data.from_deepcopy()
|
70
|
+
copied.resample_to_business_period_ends(freq=freq)
|
71
|
+
vtypes = [x == ValueType.RTRN for x in copied.tsdf.columns.get_level_values(1)]
|
72
|
+
if not any(vtypes):
|
73
|
+
copied.value_to_ret()
|
74
|
+
cldr = copied.tsdf.iloc[1:].copy()
|
75
|
+
if relabel:
|
76
|
+
if freq == "BYE":
|
77
|
+
cldr.index = [d.year for d in cldr.index]
|
78
|
+
else:
|
79
|
+
cldr.index = [d.strftime("%b %y") for d in cldr.index]
|
80
|
+
|
81
|
+
return cldr # type: ignore[no-any-return]
|
82
|
+
|
83
|
+
|
84
|
+
def report_html(
|
85
|
+
data: OpenFrame,
|
86
|
+
bar_freq: LiteralBizDayFreq = "BYE",
|
87
|
+
filename: str | None = None,
|
88
|
+
title: str | None = None,
|
89
|
+
directory: Path | None = None,
|
90
|
+
output_type: LiteralPlotlyOutput = "file",
|
91
|
+
include_plotlyjs: LiteralPlotlyJSlib = "cdn",
|
92
|
+
*,
|
93
|
+
auto_open: bool = False,
|
94
|
+
add_logo: bool = True,
|
95
|
+
vertical_legend: bool = True,
|
96
|
+
) -> tuple[Figure, str]:
|
97
|
+
"""Generate a HTML report page with line and bar plots and a table.
|
98
|
+
|
99
|
+
Parameters
|
100
|
+
----------
|
101
|
+
data: OpenFrame
|
102
|
+
The timeseries data
|
103
|
+
bar_freq: LiteralBizDayFreq
|
104
|
+
The date offset string that sets the bar plot frequency
|
105
|
+
filename: str, optional
|
106
|
+
Name of the Plotly html file
|
107
|
+
title: str, optional
|
108
|
+
The report page title
|
109
|
+
directory: DirectoryPath, optional
|
110
|
+
Directory where Plotly html file is saved
|
111
|
+
output_type: LiteralPlotlyOutput, default: "file"
|
112
|
+
Determines output type
|
113
|
+
include_plotlyjs: LiteralPlotlyJSlib, default: "cdn"
|
114
|
+
Determines how the plotly.js library is included in the output
|
115
|
+
auto_open: bool, default: True
|
116
|
+
Determines whether to open a browser window with the plot
|
117
|
+
add_logo: bool, default: True
|
118
|
+
If True a Captor logo is added to the plot
|
119
|
+
vertical_legend: bool, default: True
|
120
|
+
Determines whether to vertically align the legend's labels
|
121
|
+
|
122
|
+
Returns:
|
123
|
+
-------
|
124
|
+
tuple[plotly.go.Figure, str]
|
125
|
+
Plotly Figure and a div section or a html filename with location
|
126
|
+
|
127
|
+
"""
|
128
|
+
data.trunc_frame().value_nan_handle().to_cumret()
|
129
|
+
|
130
|
+
if data.yearfrac > 1.0:
|
131
|
+
properties = [
|
132
|
+
"geo_ret",
|
133
|
+
"vol",
|
134
|
+
"ret_vol_ratio",
|
135
|
+
"sortino_ratio",
|
136
|
+
"worst_month",
|
137
|
+
"first_indices",
|
138
|
+
"last_indices",
|
139
|
+
]
|
140
|
+
labels_init = [
|
141
|
+
"Return (CAGR)",
|
142
|
+
"Volatility",
|
143
|
+
"Sharpe Ratio",
|
144
|
+
"Sortino Ratio",
|
145
|
+
"Worst Month",
|
146
|
+
"Comparison Start",
|
147
|
+
"Comparison End",
|
148
|
+
"Jensen's Alpha",
|
149
|
+
"Information Ratio",
|
150
|
+
"Tracking Error (weekly)",
|
151
|
+
"Capture Ratio (monthly)",
|
152
|
+
"Index Beta (weekly)",
|
153
|
+
]
|
154
|
+
labels_final = [
|
155
|
+
"Return (CAGR)",
|
156
|
+
"Year-to-Date",
|
157
|
+
"Month-to-Date",
|
158
|
+
"Volatility",
|
159
|
+
"Sharpe Ratio",
|
160
|
+
"Sortino Ratio",
|
161
|
+
"Jensen's Alpha",
|
162
|
+
"Information Ratio",
|
163
|
+
"Tracking Error (weekly)",
|
164
|
+
"Index Beta (weekly)",
|
165
|
+
"Capture Ratio (monthly)",
|
166
|
+
"Worst Month",
|
167
|
+
"Comparison Start",
|
168
|
+
"Comparison End",
|
169
|
+
]
|
170
|
+
else:
|
171
|
+
properties = [
|
172
|
+
"value_ret",
|
173
|
+
"vol",
|
174
|
+
"ret_vol_ratio",
|
175
|
+
"sortino_ratio",
|
176
|
+
"worst",
|
177
|
+
"first_indices",
|
178
|
+
"last_indices",
|
179
|
+
]
|
180
|
+
labels_init = [
|
181
|
+
"Return (simple)",
|
182
|
+
"Volatility",
|
183
|
+
"Sharpe Ratio",
|
184
|
+
"Sortino Ratio",
|
185
|
+
"Worst Day",
|
186
|
+
"Comparison Start",
|
187
|
+
"Comparison End",
|
188
|
+
"Jensen's Alpha",
|
189
|
+
"Information Ratio",
|
190
|
+
"Tracking Error (weekly)",
|
191
|
+
"Index Beta (weekly)",
|
192
|
+
]
|
193
|
+
labels_final = [
|
194
|
+
"Return (simple)",
|
195
|
+
"Year-to-Date",
|
196
|
+
"Month-to-Date",
|
197
|
+
"Volatility",
|
198
|
+
"Sharpe Ratio",
|
199
|
+
"Sortino Ratio",
|
200
|
+
"Jensen's Alpha",
|
201
|
+
"Information Ratio",
|
202
|
+
"Tracking Error (weekly)",
|
203
|
+
"Index Beta (weekly)",
|
204
|
+
"Worst Day",
|
205
|
+
"Comparison Start",
|
206
|
+
"Comparison End",
|
207
|
+
]
|
208
|
+
|
209
|
+
figure = make_subplots(
|
210
|
+
rows=2,
|
211
|
+
cols=2,
|
212
|
+
specs=[
|
213
|
+
[{"type": "xy"}, {"rowspan": 2, "type": "table"}],
|
214
|
+
[{"type": "xy"}, None],
|
215
|
+
],
|
216
|
+
)
|
217
|
+
|
218
|
+
for item, lbl in enumerate(data.columns_lvl_zero):
|
219
|
+
figure.add_scatter(
|
220
|
+
x=data.tsdf.index,
|
221
|
+
y=data.tsdf.iloc[:, item],
|
222
|
+
hovertemplate="%{y:.2%}<br>%{x|%Y-%m-%d}",
|
223
|
+
line={"width": 2.5, "dash": "solid"},
|
224
|
+
mode="lines",
|
225
|
+
name=lbl,
|
226
|
+
showlegend=True,
|
227
|
+
row=1,
|
228
|
+
col=1,
|
229
|
+
)
|
230
|
+
|
231
|
+
quarter_of_year = 0.25
|
232
|
+
if data.yearfrac < quarter_of_year:
|
233
|
+
tmp = data.from_deepcopy()
|
234
|
+
bdf = tmp.value_to_ret().tsdf.iloc[1:]
|
235
|
+
else:
|
236
|
+
bdf = calendar_period_returns(data, freq=bar_freq)
|
237
|
+
|
238
|
+
for item in range(data.item_count):
|
239
|
+
figure.add_bar(
|
240
|
+
x=bdf.index,
|
241
|
+
y=bdf.iloc[:, item],
|
242
|
+
hovertemplate="%{y:.2%}<br>%{x}",
|
243
|
+
name=bdf.iloc[:, item].name[0],
|
244
|
+
showlegend=False,
|
245
|
+
row=2,
|
246
|
+
col=1,
|
247
|
+
)
|
248
|
+
|
249
|
+
formats = [
|
250
|
+
"{:.2%}",
|
251
|
+
"{:.2%}",
|
252
|
+
"{:.2f}",
|
253
|
+
"{:.2f}",
|
254
|
+
"{:.2%}",
|
255
|
+
"{:%Y-%m-%d}",
|
256
|
+
"{:%Y-%m-%d}",
|
257
|
+
"{:.2%}",
|
258
|
+
"{:.2f}",
|
259
|
+
"{:.2%}",
|
260
|
+
"{:.2f}",
|
261
|
+
]
|
262
|
+
|
263
|
+
# noinspection PyTypeChecker
|
264
|
+
rpt_df = data.all_properties(properties=properties)
|
265
|
+
alpha_frame = data.from_deepcopy()
|
266
|
+
alpha_frame.to_cumret()
|
267
|
+
with catch_warnings():
|
268
|
+
simplefilter("ignore")
|
269
|
+
alphas: list[str | float] = [
|
270
|
+
alpha_frame.jensen_alpha(
|
271
|
+
asset=(aname, ValueType.PRICE),
|
272
|
+
market=(alpha_frame.columns_lvl_zero[-1], ValueType.PRICE),
|
273
|
+
riskfree_rate=0.0,
|
274
|
+
)
|
275
|
+
for aname in alpha_frame.columns_lvl_zero[:-1]
|
276
|
+
]
|
277
|
+
alphas.append("")
|
278
|
+
ar = DataFrame(data=alphas, index=data.tsdf.columns, columns=["Jensen's Alpha"]).T
|
279
|
+
rpt_df = concat([rpt_df, ar])
|
280
|
+
ir = data.info_ratio_func()
|
281
|
+
ir.name = "Information Ratio"
|
282
|
+
ir.iloc[-1] = None
|
283
|
+
ir = ir.to_frame().T
|
284
|
+
rpt_df = concat([rpt_df, ir])
|
285
|
+
te_frame = data.from_deepcopy()
|
286
|
+
te_frame.resample("7D")
|
287
|
+
with catch_warnings():
|
288
|
+
simplefilter("ignore")
|
289
|
+
te = te_frame.tracking_error_func()
|
290
|
+
if te.hasnans:
|
291
|
+
te = Series(
|
292
|
+
data=[""] * te_frame.item_count,
|
293
|
+
index=te_frame.tsdf.columns,
|
294
|
+
name="Tracking Error (weekly)",
|
295
|
+
)
|
296
|
+
else:
|
297
|
+
te.iloc[-1] = None
|
298
|
+
te.name = "Tracking Error (weekly)"
|
299
|
+
te = te.to_frame().T
|
300
|
+
rpt_df = concat([rpt_df, te])
|
301
|
+
|
302
|
+
if data.yearfrac > 1.0:
|
303
|
+
crm = data.from_deepcopy()
|
304
|
+
crm.resample("ME")
|
305
|
+
cru_save = Series(
|
306
|
+
data=[""] * crm.item_count,
|
307
|
+
index=crm.tsdf.columns,
|
308
|
+
name="Capture Ratio (monthly)",
|
309
|
+
)
|
310
|
+
with catch_warnings():
|
311
|
+
simplefilter("ignore")
|
312
|
+
try:
|
313
|
+
cru = crm.capture_ratio_func(ratio="both")
|
314
|
+
except ZeroDivisionError as exc: # pragma: no cover
|
315
|
+
msg = f"Capture ratio calculation error: {exc!s}" # pragma: no cover
|
316
|
+
logger.warning(msg=msg) # pragma: no cover
|
317
|
+
cru = cru_save # pragma: no cover
|
318
|
+
if cru.hasnans:
|
319
|
+
cru = cru_save
|
320
|
+
else:
|
321
|
+
cru.iloc[-1] = None
|
322
|
+
cru.name = "Capture Ratio (monthly)"
|
323
|
+
cru = cru.to_frame().T
|
324
|
+
rpt_df = concat([rpt_df, cru])
|
325
|
+
formats.append("{:.2f}")
|
326
|
+
beta_frame = data.from_deepcopy()
|
327
|
+
beta_frame.resample("7D").value_nan_handle("drop")
|
328
|
+
beta_frame.to_cumret()
|
329
|
+
betas: list[str | float] = [
|
330
|
+
beta_frame.beta(
|
331
|
+
asset=(bname, ValueType.PRICE),
|
332
|
+
market=(beta_frame.columns_lvl_zero[-1], ValueType.PRICE),
|
333
|
+
)
|
334
|
+
for bname in beta_frame.columns_lvl_zero[:-1]
|
335
|
+
]
|
336
|
+
# noinspection PyTypeChecker
|
337
|
+
betas.append("")
|
338
|
+
br = DataFrame(
|
339
|
+
data=betas,
|
340
|
+
index=data.tsdf.columns,
|
341
|
+
columns=["Index Beta (weekly)"],
|
342
|
+
).T
|
343
|
+
rpt_df = concat([rpt_df, br])
|
344
|
+
|
345
|
+
for item, f in zip(rpt_df.index, formats, strict=False):
|
346
|
+
rpt_df.loc[item] = rpt_df.loc[item].apply(
|
347
|
+
lambda x, fmt=f: x if (isinstance(x, str) or x is None) else fmt.format(x), # type: ignore[return-value]
|
348
|
+
)
|
349
|
+
|
350
|
+
rpt_df.index = labels_init
|
351
|
+
|
352
|
+
this_year = data.last_idx.year
|
353
|
+
this_month = data.last_idx.month
|
354
|
+
ytd = cast("Series[float]", data.value_ret_calendar_period(year=this_year)).map(
|
355
|
+
"{:.2%}".format
|
356
|
+
)
|
357
|
+
ytd.name = "Year-to-Date"
|
358
|
+
mtd = cast(
|
359
|
+
"Series[float]",
|
360
|
+
data.value_ret_calendar_period(year=this_year, month=this_month),
|
361
|
+
).map(
|
362
|
+
"{:.2%}".format,
|
363
|
+
)
|
364
|
+
mtd.name = "Month-to-Date"
|
365
|
+
ytd = ytd.to_frame().T
|
366
|
+
mtd = mtd.to_frame().T
|
367
|
+
rpt_df = concat([rpt_df, ytd])
|
368
|
+
rpt_df = concat([rpt_df, mtd])
|
369
|
+
rpt_df = rpt_df.reindex(labels_final)
|
370
|
+
|
371
|
+
rpt_df.index = [f"<b>{x}</b>" for x in rpt_df.index]
|
372
|
+
rpt_df = rpt_df.reset_index()
|
373
|
+
|
374
|
+
colmns = ["", *data.columns_lvl_zero]
|
375
|
+
columns = [f"<b>{x}</b>" for x in colmns]
|
376
|
+
aligning = ["left"] + ["center"] * (len(columns) - 1)
|
377
|
+
|
378
|
+
col_even_color = "lightgrey"
|
379
|
+
col_odd_color = "white"
|
380
|
+
color_lst = ["grey"] + [col_odd_color] * (data.item_count - 1) + [col_even_color]
|
381
|
+
|
382
|
+
tablevalues = rpt_df.transpose().to_numpy().tolist()
|
383
|
+
cleanedtablevalues = list(tablevalues)[:-1]
|
384
|
+
cleanedcol = [
|
385
|
+
valu if valu not in ["nan", "nan%"] else "" for valu in tablevalues[-1]
|
386
|
+
]
|
387
|
+
cleanedtablevalues.append(cleanedcol)
|
388
|
+
|
389
|
+
figure.add_table(
|
390
|
+
header={
|
391
|
+
"values": columns,
|
392
|
+
"align": "center",
|
393
|
+
"fill_color": "grey",
|
394
|
+
"font": {"color": "white"},
|
395
|
+
},
|
396
|
+
cells={
|
397
|
+
"values": cleanedtablevalues,
|
398
|
+
"align": aligning,
|
399
|
+
"height": 25,
|
400
|
+
"fill_color": color_lst,
|
401
|
+
"font": {"color": ["white"] + ["black"] * len(columns)},
|
402
|
+
},
|
403
|
+
row=1,
|
404
|
+
col=2,
|
405
|
+
)
|
406
|
+
|
407
|
+
if directory:
|
408
|
+
dirpath = Path(directory).resolve()
|
409
|
+
elif Path.home().joinpath("Documents").exists():
|
410
|
+
dirpath = Path.home() / "Documents"
|
411
|
+
else:
|
412
|
+
dirpath = Path(stack()[1].filename).parent
|
413
|
+
|
414
|
+
if not filename:
|
415
|
+
filename = "".join(choice(ascii_letters) for _ in range(6)) + ".html"
|
416
|
+
|
417
|
+
plotfile = dirpath / filename
|
418
|
+
|
419
|
+
fig, logo = load_plotly_dict()
|
420
|
+
|
421
|
+
if add_logo:
|
422
|
+
figure.add_layout_image(logo)
|
423
|
+
|
424
|
+
figure.update_layout(fig.get("layout"))
|
425
|
+
colorway: list[str] = cast("dict[str, list[str]]", fig["layout"]).get("colorway")
|
426
|
+
|
427
|
+
if vertical_legend:
|
428
|
+
legend = {
|
429
|
+
"yanchor": "bottom",
|
430
|
+
"y": -0.04,
|
431
|
+
"xanchor": "right",
|
432
|
+
"x": 0.98,
|
433
|
+
"orientation": "v",
|
434
|
+
}
|
435
|
+
else:
|
436
|
+
legend = {
|
437
|
+
"yanchor": "bottom",
|
438
|
+
"y": -0.2,
|
439
|
+
"xanchor": "right",
|
440
|
+
"x": 0.98,
|
441
|
+
"orientation": "h",
|
442
|
+
}
|
443
|
+
|
444
|
+
figure.update_layout(
|
445
|
+
legend=legend,
|
446
|
+
colorway=colorway[: data.item_count],
|
447
|
+
)
|
448
|
+
figure.update_xaxes(gridcolor="#EEEEEE", automargin=True, tickangle=-45)
|
449
|
+
figure.update_yaxes(tickformat=".2%", gridcolor="#EEEEEE", automargin=True)
|
450
|
+
|
451
|
+
if isinstance(title, str):
|
452
|
+
figure.update_layout(
|
453
|
+
{"title": {"text": f"<b>{title}</b><br>", "font": {"size": 36}}},
|
454
|
+
)
|
455
|
+
|
456
|
+
if output_type == "file":
|
457
|
+
plot(
|
458
|
+
figure_or_data=figure,
|
459
|
+
filename=str(plotfile),
|
460
|
+
auto_open=auto_open,
|
461
|
+
auto_play=False,
|
462
|
+
link_text="",
|
463
|
+
include_plotlyjs=cast("bool", include_plotlyjs),
|
464
|
+
output_type=output_type,
|
465
|
+
config=fig["config"],
|
466
|
+
)
|
467
|
+
string_output = str(plotfile)
|
468
|
+
else:
|
469
|
+
div_id = filename.split(sep=".")[0]
|
470
|
+
string_output = to_html(
|
471
|
+
fig=figure,
|
472
|
+
div_id=div_id,
|
473
|
+
auto_play=False,
|
474
|
+
full_html=False,
|
475
|
+
include_plotlyjs=cast("bool", include_plotlyjs),
|
476
|
+
config=fig["config"],
|
477
|
+
)
|
478
|
+
|
479
|
+
return figure, string_output
|
openseries/series.py
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
-
"""Defining the OpenTimeSeries class.
|
1
|
+
"""Defining the OpenTimeSeries class.
|
2
|
+
|
3
|
+
Copyright (c) Captor Fund Management AB. This file is part of the openseries project.
|
4
|
+
|
5
|
+
Licensed under the BSD 3-Clause License. You may obtain a copy of the License at:
|
6
|
+
https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
|
7
|
+
SPDX-License-Identifier: BSD-3-Clause
|
8
|
+
"""
|
2
9
|
|
3
10
|
# mypy: disable-error-code="no-any-return"
|
4
11
|
from __future__ import annotations
|
openseries/simulation.py
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
-
"""Defining the ReturnSimulation class.
|
1
|
+
"""Defining the ReturnSimulation class.
|
2
|
+
|
3
|
+
Copyright (c) Captor Fund Management AB. This file is part of the openseries project.
|
4
|
+
|
5
|
+
Licensed under the BSD 3-Clause License. You may obtain a copy of the License at:
|
6
|
+
https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
|
7
|
+
SPDX-License-Identifier: BSD-3-Clause
|
8
|
+
"""
|
2
9
|
|
3
10
|
# mypy: disable-error-code="no-any-return"
|
4
11
|
from __future__ import annotations
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: openseries
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.9.1
|
4
4
|
Summary: Tools for analyzing financial timeseries.
|
5
5
|
License: # BSD 3-Clause License
|
6
6
|
|
@@ -46,13 +46,12 @@ Classifier: Natural Language :: English
|
|
46
46
|
Classifier: Development Status :: 5 - Production/Stable
|
47
47
|
Classifier: Operating System :: OS Independent
|
48
48
|
Classifier: Framework :: Pydantic
|
49
|
+
Requires-Dist: exchange-calendars (>=4.8,<6.0)
|
49
50
|
Requires-Dist: holidays (>=0.30,<1.0)
|
50
|
-
Requires-Dist: numpy (>=1.23.2,<3.0.0)
|
51
|
+
Requires-Dist: numpy (>=1.23.2,!=2.3.0,<3.0.0)
|
51
52
|
Requires-Dist: openpyxl (>=3.1.2,<5.0.0)
|
52
53
|
Requires-Dist: pandas (>=2.1.2,<3.0.0)
|
53
|
-
Requires-Dist: pandas-market-calendars (>=5.1.0,<7.0.0)
|
54
54
|
Requires-Dist: plotly (>=5.18.0,<7.0.0)
|
55
|
-
Requires-Dist: pyarrow (>=14.0.2,<21.0.0)
|
56
55
|
Requires-Dist: pydantic (>=2.5.2,<3.0.0)
|
57
56
|
Requires-Dist: python-dateutil (>=2.8.2,<4.0.0)
|
58
57
|
Requires-Dist: requests (>=2.20.0,<3.0.0)
|
@@ -75,7 +74,7 @@ Description-Content-Type: text/markdown
|
|
75
74
|

|
76
75
|
[](https://www.python.org/)
|
77
76
|
[](https://github.com/CaptorAB/openseries/actions/workflows/test.yml)
|
78
|
-
[](https://codecov.io/gh/CaptorAB/openseries/branch/master)
|
79
78
|
[](https://python-poetry.org/)
|
80
79
|
[](https://beta.ruff.rs/docs/)
|
81
80
|
[](https://github.com/CaptorAB/openseries/blob/master/LICENSE.md)
|
@@ -118,35 +117,9 @@ _,_=series.plot_series()
|
|
118
117
|
|
119
118
|
```
|
120
119
|
|
121
|
-
### Sample output using the
|
120
|
+
### Sample output using the report_html() function:
|
122
121
|
|
123
|
-
|
124
|
-
Scilla Global Equity C (simulation+fund) Global Low Volatility index, SEK
|
125
|
-
ValueType.PRICE ValueType.PRICE
|
126
|
-
Total return 3.641282 1.946319
|
127
|
-
Arithmetic return 0.096271 0.069636
|
128
|
-
Geometric return 0.093057 0.06464
|
129
|
-
Volatility 0.120279 0.117866
|
130
|
-
Return vol ratio 0.800396 0.59081
|
131
|
-
Downside deviation 0.085956 0.086723
|
132
|
-
Sortino ratio 1.119993 0.802975
|
133
|
-
Positive share 0.541783 0.551996
|
134
|
-
Worst -0.071616 -0.089415
|
135
|
-
Worst month -0.122503 -0.154485
|
136
|
-
Max drawdown -0.309849 -0.435444
|
137
|
-
Max drawdown in cal yr -0.309849 -0.348681
|
138
|
-
Max drawdown dates 2020-03-23 2009-03-09
|
139
|
-
CVaR 95.0% -0.01793 -0.018429
|
140
|
-
VaR 95.0% -0.011365 -0.010807
|
141
|
-
Imp vol from VaR 95% 0.109204 0.103834
|
142
|
-
Z-score 0.587905 0.103241
|
143
|
-
Skew -0.650782 -0.888109
|
144
|
-
Kurtosis 8.511166 17.527367
|
145
|
-
observations 4309 4309
|
146
|
-
span of days 6301 6301
|
147
|
-
first indices 2006-01-03 2006-01-03
|
148
|
-
last indices 2023-04-05 2023-04-05
|
149
|
-
```
|
122
|
+
<img src="./captor_plot_image.png" alt="Two Assets Compared" width="1000" />
|
150
123
|
|
151
124
|
## Development Instructions
|
152
125
|
|
@@ -171,9 +144,8 @@ cd openseries
|
|
171
144
|
```bash
|
172
145
|
git clone https://github.com/CaptorAB/openseries.git
|
173
146
|
cd openseries
|
174
|
-
make
|
175
|
-
source source_me
|
176
147
|
make install
|
148
|
+
source source_me
|
177
149
|
|
178
150
|
```
|
179
151
|
|
@@ -220,12 +192,13 @@ make lint
|
|
220
192
|
|
221
193
|
### On some files in the project
|
222
194
|
|
223
|
-
| File | Description
|
224
|
-
|
225
|
-
| [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.
|
226
|
-
| [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.
|
227
|
-
| [portfoliotools.py](https://github.com/CaptorAB/openseries/blob/master/openseries/portfoliotools.py) | Defines functions to simulate, optimize, and plot portfolios.
|
228
|
-
| [
|
195
|
+
| File | Description |
|
196
|
+
|:-----------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
197
|
+
| [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. |
|
198
|
+
| [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. |
|
199
|
+
| [portfoliotools.py](https://github.com/CaptorAB/openseries/blob/master/openseries/portfoliotools.py) | Defines functions to simulate, optimize, and plot portfolios. |
|
200
|
+
| [report.py](https://github.com/CaptorAB/openseries/blob/master/openseries/report.py) | Defines the _report_html_ function that is used to create a landscape orientation report on at least two assets. All preceding assets will be measured against the last asset in the input OpenFrame. |
|
201
|
+
| [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 |
|
229
202
|
|
230
203
|
### Class methods used to construct objects.
|
231
204
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
openseries/__init__.py,sha256=WAh79oE-ceGG_yl4nBukkp3UPvmLk4u_GySL2xOKbxE,1375
|
2
|
+
openseries/_common_model.py,sha256=1RdWhdoFnEndhcKuYw7IMuxkWmITIsfFE7KgvC0p0tU,83060
|
3
|
+
openseries/_risk.py,sha256=8XKZWWXrECo0Vd9r2kbcn4dzyPuo93DAEO8eSkv4w20,2357
|
4
|
+
openseries/datefixer.py,sha256=FBe0zEcCDbrMPatN9OvaKqXJd9EQOqXzknQQ1N3Ji0s,15774
|
5
|
+
openseries/frame.py,sha256=RKt9PA6wrETb1eBpWhJgLyktnrEcskWd-BRmHGenqBw,55925
|
6
|
+
openseries/load_plotly.py,sha256=VBrjKUpips-yFSySIHYbVTuUfGkSFsulgZd2LPwmYVE,2271
|
7
|
+
openseries/owntypes.py,sha256=pzT-DHtCHbEwDDYjGTT_6gI71TEA9gtfeAMw0038VqQ,9612
|
8
|
+
openseries/plotly_captor_logo.json,sha256=F5nhMzEyxKywtjvQqMTKgKRCJQYMDIiBgDSxdte8Clo,178
|
9
|
+
openseries/plotly_layouts.json,sha256=9tKAeittrjwJWhBMV8SnCDAWdhgbVnUqXcN6P_J_bos,1433
|
10
|
+
openseries/portfoliotools.py,sha256=krEgw9DdhbKEYKGAt-8KERzCrXXi6ViSbpa-s1Em0hw,19566
|
11
|
+
openseries/report.py,sha256=8CN-rCc_grDAPw2k-oU00R3M_eIjSGkquQb-1ZDb5MA,14025
|
12
|
+
openseries/series.py,sha256=DyM5rdwTSz0Xvq7NuX9HEkgwsSsfWie-bU4ycjE-r2s,28536
|
13
|
+
openseries/simulation.py,sha256=wRw8yKphWLmgsjOjLZd2K1rbEl-mZw3fcJswixTvZos,14402
|
14
|
+
openseries-1.9.1.dist-info/LICENSE.md,sha256=wNupG-KLsG0aTncb_SMNDh1ExtrKXlpxSJ6RC-g-SWs,1516
|
15
|
+
openseries-1.9.1.dist-info/METADATA,sha256=0XQ208o-zf9harMeOzcnExcIiJRE3eW6ZHipYd0BjCI,44440
|
16
|
+
openseries-1.9.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
17
|
+
openseries-1.9.1.dist-info/RECORD,,
|
@@ -1,16 +0,0 @@
|
|
1
|
-
openseries/__init__.py,sha256=dKw_wEfgrCwwV1IRljesrtxjE9AVFwTyhE8k4CFIck8,1053
|
2
|
-
openseries/_common_model.py,sha256=oW-jM8CCAS4E7R-E7hPHDUkMJ5i2sc1DRGxvRB498SA,82789
|
3
|
-
openseries/_risk.py,sha256=lZzoP5yjq9vHtKhYe7kU3-iG8rADcu00bkT9kIgsi_E,2086
|
4
|
-
openseries/datefixer.py,sha256=7ugMIEKf5lhifd1FlC_aHlHt_Fwvof3aRSLaHM2fvR8,15525
|
5
|
-
openseries/frame.py,sha256=H3gAIKTDy5aStKTjBtudsJX15knH0t-8NfdD26JVu3Y,55654
|
6
|
-
openseries/load_plotly.py,sha256=VGDdS8ojPQK7AQr-dGi9IfShi5O0EfjM8kUQrJhG_Zw,2000
|
7
|
-
openseries/owntypes.py,sha256=j5KUbHqJTKyZPaQOurojtf7RoPgk5TC8A1KEtMO6EN0,9341
|
8
|
-
openseries/plotly_captor_logo.json,sha256=F5nhMzEyxKywtjvQqMTKgKRCJQYMDIiBgDSxdte8Clo,178
|
9
|
-
openseries/plotly_layouts.json,sha256=9tKAeittrjwJWhBMV8SnCDAWdhgbVnUqXcN6P_J_bos,1433
|
10
|
-
openseries/portfoliotools.py,sha256=6bgz64-B6qJVrHAE-pLp8JJCmJkO_JAExHL5G3AwPWE,19295
|
11
|
-
openseries/series.py,sha256=xYU4c1MA02AaFko5q72JoVDEKs60zDmxkfG4P_uCpOY,28265
|
12
|
-
openseries/simulation.py,sha256=LhKfA32uskMNr90V9r1_uYKC7FZgWRcdLzw2--D1qUM,14131
|
13
|
-
openseries-1.8.4.dist-info/LICENSE.md,sha256=wNupG-KLsG0aTncb_SMNDh1ExtrKXlpxSJ6RC-g-SWs,1516
|
14
|
-
openseries-1.8.4.dist-info/METADATA,sha256=D6rlANyAmZ72EonbQTMyjE10ctd1MuJqQ3nVXbqNO3A,46550
|
15
|
-
openseries-1.8.4.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
16
|
-
openseries-1.8.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|