openseries 1.8.3__py3-none-any.whl → 1.9.0__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 +55 -5
- openseries/_risk.py +8 -1
- openseries/datefixer.py +127 -33
- openseries/frame.py +10 -7
- openseries/load_plotly.py +8 -1
- openseries/owntypes.py +12 -11
- openseries/portfoliotools.py +8 -1
- openseries/report.py +479 -0
- openseries/series.py +32 -1
- openseries/simulation.py +12 -1
- {openseries-1.8.3.dist-info → openseries-1.9.0.dist-info}/METADATA +46 -73
- openseries-1.9.0.dist-info/RECORD +17 -0
- openseries-1.8.3.dist-info/RECORD +0 -16
- {openseries-1.8.3.dist-info → openseries-1.9.0.dist-info}/LICENSE.md +0 -0
- {openseries-1.8.3.dist-info → openseries-1.9.0.dist-info}/WHEEL +0 -0
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
|
@@ -45,6 +52,7 @@ from .owntypes import (
|
|
45
52
|
LiteralBizDayFreq,
|
46
53
|
LiteralPandasReindexMethod,
|
47
54
|
LiteralSeriesProps,
|
55
|
+
MarketsNotStringNorListStrError,
|
48
56
|
OpenTimeSeriesPropertiesList,
|
49
57
|
Self,
|
50
58
|
ValueListType,
|
@@ -91,6 +99,8 @@ class OpenTimeSeries(_CommonModel):
|
|
91
99
|
ISO 4217 currency code of the user's home currency
|
92
100
|
countries: CountriesType, default: "SE"
|
93
101
|
(List of) country code(s) according to ISO 3166-1 alpha-2
|
102
|
+
markets: list[str] | str, optional
|
103
|
+
(List of) markets code(s) according to pandas-market-calendars
|
94
104
|
isin : str, optional
|
95
105
|
ISO 6166 identifier code of the associated instrument
|
96
106
|
label : str, optional
|
@@ -109,6 +119,7 @@ class OpenTimeSeries(_CommonModel):
|
|
109
119
|
currency: CurrencyStringType
|
110
120
|
domestic: CurrencyStringType = "SEK"
|
111
121
|
countries: CountriesType = "SE"
|
122
|
+
markets: list[str] | str | None = None # type: ignore[assignment]
|
112
123
|
isin: str | None = None
|
113
124
|
label: str | None = None
|
114
125
|
|
@@ -126,6 +137,25 @@ class OpenTimeSeries(_CommonModel):
|
|
126
137
|
_ = Countries(countryinput=value)
|
127
138
|
return value
|
128
139
|
|
140
|
+
@field_validator("markets", mode="before") # type: ignore[misc]
|
141
|
+
@classmethod
|
142
|
+
def _validate_markets(
|
143
|
+
cls, value: list[str] | str | None
|
144
|
+
) -> list[str] | str | None:
|
145
|
+
"""Pydantic validator to ensure markets field is validated."""
|
146
|
+
msg = (
|
147
|
+
"'markets' must be a string or list of strings, "
|
148
|
+
f"got {type(value).__name__!r}"
|
149
|
+
)
|
150
|
+
if value is None or isinstance(value, str):
|
151
|
+
return value
|
152
|
+
if isinstance(value, list):
|
153
|
+
if all(isinstance(item, str) for item in value) and len(value) != 0:
|
154
|
+
return value
|
155
|
+
item_msg = "All items in 'markets' must be strings."
|
156
|
+
raise MarketsNotStringNorListStrError(item_msg)
|
157
|
+
raise MarketsNotStringNorListStrError(msg)
|
158
|
+
|
129
159
|
@model_validator(mode="after") # type: ignore[misc,unused-ignore]
|
130
160
|
def _dates_and_values_validate(self: Self) -> Self:
|
131
161
|
"""Pydantic validator to ensure dates and values are validated."""
|
@@ -587,6 +617,7 @@ class OpenTimeSeries(_CommonModel):
|
|
587
617
|
data=self.tsdf,
|
588
618
|
freq=freq,
|
589
619
|
countries=self.countries,
|
620
|
+
markets=self.markets,
|
590
621
|
)
|
591
622
|
self.tsdf = self.tsdf.reindex([deyt.date() for deyt in dates], method=method)
|
592
623
|
return self
|
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
|
@@ -417,6 +424,7 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
|
|
417
424
|
start: dt.date | None = None,
|
418
425
|
end: dt.date | None = None,
|
419
426
|
countries: CountriesType = "SE",
|
427
|
+
markets: list[str] | str | None = None,
|
420
428
|
) -> DataFrame:
|
421
429
|
"""Create a pandas.DataFrame from simulation(s).
|
422
430
|
|
@@ -430,6 +438,8 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
|
|
430
438
|
Date when the simulation ends
|
431
439
|
countries: CountriesType, default: "SE"
|
432
440
|
(List of) country code(s) according to ISO 3166-1 alpha-2
|
441
|
+
markets: list[str] | str, optional
|
442
|
+
(List of) markets code(s) according to pandas-market-calendars
|
433
443
|
|
434
444
|
Returns:
|
435
445
|
-------
|
@@ -442,6 +452,7 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
|
|
442
452
|
start=start,
|
443
453
|
end=end,
|
444
454
|
countries=countries,
|
455
|
+
markets=markets,
|
445
456
|
)
|
446
457
|
|
447
458
|
if self.number_of_sims == 1:
|