openseries 1.9.4__py3-none-any.whl → 1.9.6__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/_common_model.py +373 -451
- openseries/datefixer.py +8 -6
- openseries/frame.py +110 -93
- openseries/owntypes.py +48 -47
- openseries/plotly_layouts.json +1 -1
- openseries/portfoliotools.py +10 -12
- openseries/report.py +66 -61
- openseries/series.py +51 -37
- openseries/simulation.py +3 -3
- {openseries-1.9.4.dist-info → openseries-1.9.6.dist-info}/METADATA +4 -3
- openseries-1.9.6.dist-info/RECORD +17 -0
- {openseries-1.9.4.dist-info → openseries-1.9.6.dist-info}/WHEEL +1 -1
- openseries-1.9.4.dist-info/RECORD +0 -17
- {openseries-1.9.4.dist-info → openseries-1.9.6.dist-info/licenses}/LICENSE.md +0 -0
openseries/portfoliotools.py
CHANGED
@@ -7,7 +7,6 @@ https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
|
|
7
7
|
SPDX-License-Identifier: BSD-3-Clause
|
8
8
|
"""
|
9
9
|
|
10
|
-
# mypy: disable-error-code="assignment"
|
11
10
|
from __future__ import annotations
|
12
11
|
|
13
12
|
from inspect import stack
|
@@ -36,7 +35,7 @@ from pandas import (
|
|
36
35
|
from plotly.graph_objs import Figure # type: ignore[import-untyped]
|
37
36
|
from plotly.io import to_html # type: ignore[import-untyped]
|
38
37
|
from plotly.offline import plot # type: ignore[import-untyped]
|
39
|
-
from scipy.optimize import minimize
|
38
|
+
from scipy.optimize import minimize
|
40
39
|
|
41
40
|
from .load_plotly import load_plotly_dict
|
42
41
|
from .owntypes import (
|
@@ -143,7 +142,7 @@ def efficient_frontier(
|
|
143
142
|
eframe: OpenFrame,
|
144
143
|
num_ports: int = 5000,
|
145
144
|
seed: int = 71,
|
146
|
-
bounds: tuple[tuple[float]] | None = None,
|
145
|
+
bounds: tuple[tuple[float, float], ...] | None = None,
|
147
146
|
frontier_points: int = 200,
|
148
147
|
minimize_method: LiteralMinimizeMethods = "SLSQP",
|
149
148
|
*,
|
@@ -159,7 +158,7 @@ def efficient_frontier(
|
|
159
158
|
Number of possible portfolios to simulate
|
160
159
|
seed: int, default: 71
|
161
160
|
The seed for the random process
|
162
|
-
bounds: tuple[tuple[float]], optional
|
161
|
+
bounds: tuple[tuple[float, float], ...], optional
|
163
162
|
The range of minumum and maximum allowed allocations for each asset
|
164
163
|
frontier_points: int, default: 200
|
165
164
|
number of points along frontier to optimize
|
@@ -253,7 +252,7 @@ def efficient_frontier(
|
|
253
252
|
bounds = tuple((0.0, 1.0) for _ in range(eframe.item_count))
|
254
253
|
init_guess = array(eframe.weights)
|
255
254
|
|
256
|
-
opt_results = minimize(
|
255
|
+
opt_results = minimize( # type: ignore[call-overload]
|
257
256
|
fun=_neg_sharpe,
|
258
257
|
x0=init_guess,
|
259
258
|
method=minimize_method,
|
@@ -288,7 +287,7 @@ def efficient_frontier(
|
|
288
287
|
),
|
289
288
|
)
|
290
289
|
|
291
|
-
result = minimize(
|
290
|
+
result = minimize( # type: ignore[call-overload]
|
292
291
|
fun=_minimize_volatility,
|
293
292
|
x0=init_guess,
|
294
293
|
method=minimize_method,
|
@@ -333,7 +332,7 @@ def constrain_optimized_portfolios(
|
|
333
332
|
portfolioname: str = "Current Portfolio",
|
334
333
|
simulations: int = 10000,
|
335
334
|
curve_points: int = 200,
|
336
|
-
bounds: tuple[tuple[float]] | None = None,
|
335
|
+
bounds: tuple[tuple[float, float], ...] | None = None,
|
337
336
|
minimize_method: LiteralMinimizeMethods = "SLSQP",
|
338
337
|
) -> tuple[OpenFrame, OpenTimeSeries, OpenFrame, OpenTimeSeries]:
|
339
338
|
"""Constrain optimized portfolios to those that improve on the current one.
|
@@ -350,7 +349,7 @@ def constrain_optimized_portfolios(
|
|
350
349
|
Number of possible portfolios to simulate
|
351
350
|
curve_points: int, default: 200
|
352
351
|
Number of optimal portfolios on the efficient frontier
|
353
|
-
bounds: tuple[tuple[float]], optional
|
352
|
+
bounds: tuple[tuple[float, float], ...], optional
|
354
353
|
The range of minumum and maximum allowed allocations for each asset
|
355
354
|
minimize_method: LiteralMinimizeMethods, default: SLSQP
|
356
355
|
The method passed into the scipy.minimize function
|
@@ -367,7 +366,7 @@ def constrain_optimized_portfolios(
|
|
367
366
|
if not bounds:
|
368
367
|
bounds = tuple((0.0, 1.0) for _ in range(data.item_count))
|
369
368
|
|
370
|
-
front_frame,
|
369
|
+
front_frame, _, _ = efficient_frontier(
|
371
370
|
eframe=data,
|
372
371
|
num_ports=simulations,
|
373
372
|
frontier_points=curve_points,
|
@@ -441,14 +440,13 @@ def prepare_plot_data(
|
|
441
440
|
for wgt, nm in zip(optimized[3:], assets.columns_lvl_zero, strict=True)
|
442
441
|
]
|
443
442
|
opt_text = "<br><br>Weights:<br>" + "<br>".join(opt_text_list)
|
444
|
-
vol: Series[float] = assets.vol
|
445
443
|
plotframe = DataFrame(
|
446
444
|
data=[
|
447
445
|
assets.arithmetic_ret,
|
448
|
-
vol,
|
446
|
+
assets.vol,
|
449
447
|
Series(
|
450
448
|
data=[""] * assets.item_count,
|
451
|
-
index=vol.index,
|
449
|
+
index=assets.vol.index,
|
452
450
|
),
|
453
451
|
],
|
454
452
|
index=["ret", "stdev", "text"],
|
openseries/report.py
CHANGED
@@ -7,7 +7,6 @@ https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
|
|
7
7
|
SPDX-License-Identifier: BSD-3-Clause
|
8
8
|
"""
|
9
9
|
|
10
|
-
# mypy: disable-error-code="assignment"
|
11
10
|
from __future__ import annotations
|
12
11
|
|
13
12
|
from inspect import stack
|
@@ -20,20 +19,21 @@ from warnings import catch_warnings, simplefilter
|
|
20
19
|
|
21
20
|
if TYPE_CHECKING: # pragma: no cover
|
22
21
|
from pandas import Series
|
23
|
-
from plotly.graph_objs import Figure
|
22
|
+
from plotly.graph_objs import Figure # type: ignore[import-untyped]
|
24
23
|
|
25
24
|
from .frame import OpenFrame
|
26
25
|
from .owntypes import LiteralPlotlyJSlib, LiteralPlotlyOutput
|
27
26
|
|
28
27
|
|
29
|
-
from pandas import DataFrame, Series, Timestamp, concat
|
30
|
-
from plotly.io import to_html
|
31
|
-
from plotly.offline import plot
|
32
|
-
from plotly.subplots import make_subplots
|
28
|
+
from pandas import DataFrame, Index, Series, Timestamp, concat
|
29
|
+
from plotly.io import to_html # type: ignore[import-untyped]
|
30
|
+
from plotly.offline import plot # type: ignore[import-untyped]
|
31
|
+
from plotly.subplots import make_subplots # type: ignore[import-untyped]
|
33
32
|
|
34
33
|
from .load_plotly import load_plotly_dict
|
35
34
|
from .owntypes import (
|
36
35
|
LiteralBizDayFreq,
|
36
|
+
LiteralFrameProps,
|
37
37
|
ValueType,
|
38
38
|
)
|
39
39
|
|
@@ -72,15 +72,15 @@ def calendar_period_returns(
|
|
72
72
|
cldr = copied.tsdf.iloc[1:].copy()
|
73
73
|
if relabel:
|
74
74
|
if freq.upper() == "BYE":
|
75
|
-
cldr.index = [d.year for d in cldr.index]
|
75
|
+
cldr.index = Index([d.year for d in cldr.index])
|
76
76
|
elif freq.upper() == "BQE":
|
77
|
-
cldr.index =
|
78
|
-
Timestamp(d).to_period("Q").strftime("Q%q %Y") for d in cldr.index
|
79
|
-
|
77
|
+
cldr.index = Index(
|
78
|
+
[Timestamp(d).to_period("Q").strftime("Q%q %Y") for d in cldr.index],
|
79
|
+
)
|
80
80
|
else:
|
81
|
-
cldr.index = [d.strftime("%b %y") for d in cldr.index]
|
81
|
+
cldr.index = Index([d.strftime("%b %y") for d in cldr.index])
|
82
82
|
|
83
|
-
return cldr
|
83
|
+
return cldr
|
84
84
|
|
85
85
|
|
86
86
|
def report_html(
|
@@ -127,9 +127,10 @@ def report_html(
|
|
127
127
|
Plotly Figure and a div section or a html filename with location
|
128
128
|
|
129
129
|
"""
|
130
|
-
data.
|
130
|
+
copied = data.from_deepcopy()
|
131
|
+
copied.trunc_frame().value_nan_handle().to_cumret()
|
131
132
|
|
132
|
-
if
|
133
|
+
if copied.yearfrac > 1.0:
|
133
134
|
properties = [
|
134
135
|
"geo_ret",
|
135
136
|
"vol",
|
@@ -217,10 +218,10 @@ def report_html(
|
|
217
218
|
],
|
218
219
|
)
|
219
220
|
|
220
|
-
for item, lbl in enumerate(
|
221
|
+
for item, lbl in enumerate(copied.columns_lvl_zero):
|
221
222
|
figure.add_scatter(
|
222
|
-
x=
|
223
|
-
y=
|
223
|
+
x=copied.tsdf.index,
|
224
|
+
y=copied.tsdf.iloc[:, item],
|
224
225
|
hovertemplate="%{y:.2%}<br>%{x|%Y-%m-%d}",
|
225
226
|
line={"width": 2.5, "dash": "solid"},
|
226
227
|
mode="lines",
|
@@ -231,18 +232,19 @@ def report_html(
|
|
231
232
|
)
|
232
233
|
|
233
234
|
quarter_of_year = 0.25
|
234
|
-
if
|
235
|
-
tmp =
|
235
|
+
if copied.yearfrac < quarter_of_year:
|
236
|
+
tmp = copied.from_deepcopy()
|
236
237
|
bdf = tmp.value_to_ret().tsdf.iloc[1:]
|
237
238
|
else:
|
238
|
-
bdf = calendar_period_returns(data, freq=bar_freq)
|
239
|
+
bdf = calendar_period_returns(data=copied, freq=bar_freq)
|
239
240
|
|
240
|
-
for item in range(
|
241
|
+
for item in range(copied.item_count):
|
242
|
+
col_name = cast("tuple[str, ValueType]", bdf.iloc[:, item].name)
|
241
243
|
figure.add_bar(
|
242
244
|
x=bdf.index,
|
243
245
|
y=bdf.iloc[:, item],
|
244
246
|
hovertemplate="%{y:.2%}<br>%{x}",
|
245
|
-
name=
|
247
|
+
name=col_name[0],
|
246
248
|
showlegend=False,
|
247
249
|
row=2,
|
248
250
|
col=1,
|
@@ -263,8 +265,10 @@ def report_html(
|
|
263
265
|
]
|
264
266
|
|
265
267
|
# noinspection PyTypeChecker
|
266
|
-
rpt_df =
|
267
|
-
|
268
|
+
rpt_df = copied.all_properties(
|
269
|
+
properties=cast("list[LiteralFrameProps]", properties),
|
270
|
+
)
|
271
|
+
alpha_frame = copied.from_deepcopy()
|
268
272
|
alpha_frame.to_cumret()
|
269
273
|
with catch_warnings():
|
270
274
|
simplefilter("ignore")
|
@@ -277,18 +281,22 @@ def report_html(
|
|
277
281
|
for aname in alpha_frame.columns_lvl_zero[:-1]
|
278
282
|
]
|
279
283
|
alphas.append("")
|
280
|
-
ar = DataFrame(
|
284
|
+
ar = DataFrame(
|
285
|
+
data=alphas,
|
286
|
+
index=copied.tsdf.columns,
|
287
|
+
columns=["Jensen's Alpha"],
|
288
|
+
).T
|
281
289
|
rpt_df = concat([rpt_df, ar])
|
282
|
-
ir =
|
290
|
+
ir = copied.info_ratio_func()
|
283
291
|
ir.name = "Information Ratio"
|
284
292
|
ir.iloc[-1] = None
|
285
|
-
|
286
|
-
rpt_df = concat([rpt_df,
|
287
|
-
te_frame =
|
293
|
+
ir_df = ir.to_frame().T
|
294
|
+
rpt_df = concat([rpt_df, ir_df])
|
295
|
+
te_frame = copied.from_deepcopy()
|
288
296
|
te_frame.resample("7D")
|
289
297
|
with catch_warnings():
|
290
298
|
simplefilter("ignore")
|
291
|
-
te = te_frame.tracking_error_func()
|
299
|
+
te: Series[float] | Series[str] = te_frame.tracking_error_func()
|
292
300
|
if te.hasnans:
|
293
301
|
te = Series(
|
294
302
|
data=[""] * te_frame.item_count,
|
@@ -298,11 +306,11 @@ def report_html(
|
|
298
306
|
else:
|
299
307
|
te.iloc[-1] = None
|
300
308
|
te.name = "Tracking Error (weekly)"
|
301
|
-
|
302
|
-
rpt_df = concat([rpt_df,
|
309
|
+
te_df = te.to_frame().T
|
310
|
+
rpt_df = concat([rpt_df, te_df])
|
303
311
|
|
304
|
-
if
|
305
|
-
crm =
|
312
|
+
if copied.yearfrac > 1.0:
|
313
|
+
crm = copied.from_deepcopy()
|
306
314
|
crm.resample("ME")
|
307
315
|
cru_save = Series(
|
308
316
|
data=[""] * crm.item_count,
|
@@ -312,7 +320,7 @@ def report_html(
|
|
312
320
|
with catch_warnings():
|
313
321
|
simplefilter("ignore")
|
314
322
|
try:
|
315
|
-
cru = crm.capture_ratio_func(ratio="both")
|
323
|
+
cru: Series[float] | Series[str] = crm.capture_ratio_func(ratio="both")
|
316
324
|
except ZeroDivisionError as exc: # pragma: no cover
|
317
325
|
msg = f"Capture ratio calculation error: {exc!s}" # pragma: no cover
|
318
326
|
logger.warning(msg=msg) # pragma: no cover
|
@@ -322,10 +330,10 @@ def report_html(
|
|
322
330
|
else:
|
323
331
|
cru.iloc[-1] = None
|
324
332
|
cru.name = "Capture Ratio (monthly)"
|
325
|
-
|
326
|
-
rpt_df = concat([rpt_df,
|
333
|
+
cru_df = cru.to_frame().T
|
334
|
+
rpt_df = concat([rpt_df, cru_df])
|
327
335
|
formats.append("{:.2f}")
|
328
|
-
beta_frame =
|
336
|
+
beta_frame = copied.from_deepcopy()
|
329
337
|
beta_frame.resample("7D").value_nan_handle("drop")
|
330
338
|
beta_frame.to_cumret()
|
331
339
|
betas: list[str | float] = [
|
@@ -335,51 +343,45 @@ def report_html(
|
|
335
343
|
)
|
336
344
|
for bname in beta_frame.columns_lvl_zero[:-1]
|
337
345
|
]
|
338
|
-
# noinspection PyTypeChecker
|
339
346
|
betas.append("")
|
340
347
|
br = DataFrame(
|
341
348
|
data=betas,
|
342
|
-
index=
|
349
|
+
index=copied.tsdf.columns,
|
343
350
|
columns=["Index Beta (weekly)"],
|
344
351
|
).T
|
345
352
|
rpt_df = concat([rpt_df, br])
|
346
353
|
|
347
354
|
for item, f in zip(rpt_df.index, formats, strict=False):
|
348
355
|
rpt_df.loc[item] = rpt_df.loc[item].apply(
|
349
|
-
lambda x, fmt=f: x if (isinstance(x, str) or x is None) else fmt.format(x),
|
356
|
+
lambda x, fmt=f: x if (isinstance(x, str) or x is None) else fmt.format(x),
|
350
357
|
)
|
351
358
|
|
352
|
-
rpt_df.index = labels_init
|
359
|
+
rpt_df.index = Index(labels_init)
|
353
360
|
|
354
|
-
this_year =
|
355
|
-
this_month =
|
356
|
-
ytd =
|
357
|
-
"{:.2%}".format
|
358
|
-
)
|
361
|
+
this_year = copied.last_idx.year
|
362
|
+
this_month = copied.last_idx.month
|
363
|
+
ytd = copied.value_ret_calendar_period(year=this_year).map("{:.2%}".format)
|
359
364
|
ytd.name = "Year-to-Date"
|
360
|
-
mtd =
|
361
|
-
"Series[float]",
|
362
|
-
data.value_ret_calendar_period(year=this_year, month=this_month),
|
363
|
-
).map(
|
365
|
+
mtd = copied.value_ret_calendar_period(year=this_year, month=this_month).map(
|
364
366
|
"{:.2%}".format,
|
365
367
|
)
|
366
368
|
mtd.name = "Month-to-Date"
|
367
|
-
|
368
|
-
|
369
|
-
rpt_df = concat([rpt_df,
|
370
|
-
rpt_df = concat([rpt_df,
|
369
|
+
ytd_df = ytd.to_frame().T
|
370
|
+
mtd_df = mtd.to_frame().T
|
371
|
+
rpt_df = concat([rpt_df, ytd_df])
|
372
|
+
rpt_df = concat([rpt_df, mtd_df])
|
371
373
|
rpt_df = rpt_df.reindex(labels_final)
|
372
374
|
|
373
|
-
rpt_df.index = [f"<b>{x}</b>" for x in rpt_df.index]
|
375
|
+
rpt_df.index = Index([f"<b>{x}</b>" for x in rpt_df.index])
|
374
376
|
rpt_df = rpt_df.reset_index()
|
375
377
|
|
376
|
-
colmns = ["", *
|
378
|
+
colmns = ["", *copied.columns_lvl_zero]
|
377
379
|
columns = [f"<b>{x}</b>" for x in colmns]
|
378
380
|
aligning = ["left"] + ["center"] * (len(columns) - 1)
|
379
381
|
|
380
382
|
col_even_color = "lightgrey"
|
381
383
|
col_odd_color = "white"
|
382
|
-
color_lst = ["grey"] + [col_odd_color] * (
|
384
|
+
color_lst = ["grey"] + [col_odd_color] * (copied.item_count - 1) + [col_even_color]
|
383
385
|
|
384
386
|
tablevalues = rpt_df.transpose().to_numpy().tolist()
|
385
387
|
cleanedtablevalues = list(tablevalues)[:-1]
|
@@ -424,7 +426,10 @@ def report_html(
|
|
424
426
|
figure.add_layout_image(logo)
|
425
427
|
|
426
428
|
figure.update_layout(fig.get("layout"))
|
427
|
-
colorway: list[str] = cast("dict[str, list[str]]", fig["layout"]).get(
|
429
|
+
colorway: list[str] = cast("dict[str, list[str]]", fig["layout"]).get(
|
430
|
+
"colorway",
|
431
|
+
[],
|
432
|
+
)
|
428
433
|
|
429
434
|
if vertical_legend:
|
430
435
|
legend = {
|
@@ -445,12 +450,12 @@ def report_html(
|
|
445
450
|
|
446
451
|
figure.update_layout(
|
447
452
|
legend=legend,
|
448
|
-
colorway=colorway[:
|
453
|
+
colorway=colorway[: copied.item_count],
|
449
454
|
)
|
450
455
|
figure.update_xaxes(gridcolor="#EEEEEE", automargin=True, tickangle=-45)
|
451
456
|
figure.update_yaxes(tickformat=".2%", gridcolor="#EEEEEE", automargin=True)
|
452
457
|
|
453
|
-
if
|
458
|
+
if title:
|
454
459
|
figure.update_layout(
|
455
460
|
{"title": {"text": f"<b>{title}</b><br>", "font": {"size": 36}}},
|
456
461
|
)
|
openseries/series.py
CHANGED
@@ -9,18 +9,22 @@ SPDX-License-Identifier: BSD-3-Clause
|
|
9
9
|
|
10
10
|
from __future__ import annotations
|
11
11
|
|
12
|
-
from collections.abc import Iterable
|
13
12
|
from copy import deepcopy
|
14
13
|
from logging import getLogger
|
15
14
|
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
16
15
|
|
17
16
|
if TYPE_CHECKING: # pragma: no cover
|
18
17
|
import datetime as dt
|
18
|
+
from collections.abc import Callable
|
19
|
+
|
20
|
+
from numpy.typing import NDArray
|
21
|
+
from pandas import Timestamp
|
19
22
|
|
20
23
|
from numpy import (
|
21
24
|
append,
|
22
25
|
array,
|
23
26
|
cumprod,
|
27
|
+
float64,
|
24
28
|
insert,
|
25
29
|
isnan,
|
26
30
|
log,
|
@@ -59,6 +63,9 @@ from .owntypes import (
|
|
59
63
|
ValueType,
|
60
64
|
)
|
61
65
|
|
66
|
+
FieldValidator = cast("Callable[..., Callable[..., Any]]", field_validator)
|
67
|
+
ModelValidator = cast("Callable[..., Callable[..., Any]]", model_validator)
|
68
|
+
|
62
69
|
logger = getLogger(__name__)
|
63
70
|
|
64
71
|
__all__ = ["OpenTimeSeries", "timeseries_chain"]
|
@@ -67,7 +74,7 @@ TypeOpenTimeSeries = TypeVar("TypeOpenTimeSeries", bound="OpenTimeSeries")
|
|
67
74
|
|
68
75
|
|
69
76
|
# noinspection PyUnresolvedReferences,PyNestedDecorators
|
70
|
-
class OpenTimeSeries(_CommonModel):
|
77
|
+
class OpenTimeSeries(_CommonModel[float]):
|
71
78
|
"""OpenTimeSeries objects are at the core of the openseries package.
|
72
79
|
|
73
80
|
The intended use is to allow analyses of financial timeseries.
|
@@ -123,24 +130,25 @@ class OpenTimeSeries(_CommonModel): # type: ignore[misc]
|
|
123
130
|
isin: str | None = None
|
124
131
|
label: str | None = None
|
125
132
|
|
126
|
-
@
|
133
|
+
@FieldValidator("domestic", mode="before")
|
127
134
|
@classmethod
|
128
135
|
def _validate_domestic(cls, value: CurrencyStringType) -> CurrencyStringType:
|
129
136
|
"""Pydantic validator to ensure domestic field is validated."""
|
130
137
|
_ = Currency(ccy=value)
|
131
138
|
return value
|
132
139
|
|
133
|
-
@
|
140
|
+
@FieldValidator("countries", mode="before")
|
134
141
|
@classmethod
|
135
142
|
def _validate_countries(cls, value: CountriesType) -> CountriesType:
|
136
143
|
"""Pydantic validator to ensure countries field is validated."""
|
137
144
|
_ = Countries(countryinput=value)
|
138
145
|
return value
|
139
146
|
|
140
|
-
@
|
147
|
+
@FieldValidator("markets", mode="before")
|
141
148
|
@classmethod
|
142
149
|
def _validate_markets(
|
143
|
-
cls,
|
150
|
+
cls,
|
151
|
+
value: list[str] | str | None,
|
144
152
|
) -> list[str] | str | None:
|
145
153
|
"""Pydantic validator to ensure markets field is validated."""
|
146
154
|
msg = (
|
@@ -156,7 +164,7 @@ class OpenTimeSeries(_CommonModel): # type: ignore[misc]
|
|
156
164
|
raise MarketsNotStringNorListStrError(item_msg)
|
157
165
|
raise MarketsNotStringNorListStrError(msg)
|
158
166
|
|
159
|
-
@
|
167
|
+
@ModelValidator(mode="after")
|
160
168
|
def _dates_and_values_validate(self: Self) -> Self:
|
161
169
|
"""Pydantic validator to ensure dates and values are validated."""
|
162
170
|
values_list_length = len(self.values)
|
@@ -165,9 +173,6 @@ class OpenTimeSeries(_CommonModel): # type: ignore[misc]
|
|
165
173
|
if dates_list_length != dates_set_length:
|
166
174
|
msg = "Dates are not unique"
|
167
175
|
raise ValueError(msg)
|
168
|
-
if values_list_length < 1:
|
169
|
-
msg = "There must be at least 1 value"
|
170
|
-
raise ValueError(msg)
|
171
176
|
if (
|
172
177
|
(dates_list_length != values_list_length)
|
173
178
|
or (len(self.tsdf.index) != self.tsdf.shape[0])
|
@@ -370,19 +375,17 @@ class OpenTimeSeries(_CommonModel): # type: ignore[misc]
|
|
370
375
|
An OpenTimeSeries object
|
371
376
|
|
372
377
|
"""
|
373
|
-
if
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
deltas = array(
|
382
|
-
[i.days for i in DatetimeIndex(d_range)[1:] - DatetimeIndex(d_range)[:-1]], # type: ignore[arg-type]
|
383
|
-
)
|
378
|
+
if d_range is None:
|
379
|
+
if days is not None and end_dt is not None:
|
380
|
+
d_range = DatetimeIndex(
|
381
|
+
[d.date() for d in date_range(periods=days, end=end_dt, freq="D")],
|
382
|
+
)
|
383
|
+
else:
|
384
|
+
msg = "If d_range is not provided both days and end_dt must be."
|
385
|
+
raise IncorrectArgumentComboError(msg)
|
386
|
+
deltas = array([i.days for i in d_range[1:] - d_range[:-1]])
|
384
387
|
arr: list[float] = list(cumprod(insert(1 + deltas * rate / 365, 0, 1.0)))
|
385
|
-
dates = [d.strftime("%Y-%m-%d") for d in
|
388
|
+
dates = [d.strftime("%Y-%m-%d") for d in d_range]
|
386
389
|
|
387
390
|
return cls(
|
388
391
|
timeseries_id="",
|
@@ -469,12 +472,13 @@ class OpenTimeSeries(_CommonModel): # type: ignore[misc]
|
|
469
472
|
The returns of the values in the series
|
470
473
|
|
471
474
|
"""
|
475
|
+
# noinspection PyCallingNonCallable
|
472
476
|
returns = self.tsdf.ffill().pct_change()
|
473
477
|
returns.iloc[0] = 0
|
474
478
|
self.valuetype = ValueType.RTRN
|
475
479
|
arrays = [[self.label], [self.valuetype]]
|
476
480
|
returns.columns = MultiIndex.from_arrays(
|
477
|
-
arrays=arrays # type: ignore[arg-type]
|
481
|
+
arrays=arrays, # type: ignore[arg-type]
|
478
482
|
)
|
479
483
|
self.tsdf = returns.copy()
|
480
484
|
return self
|
@@ -549,12 +553,16 @@ class OpenTimeSeries(_CommonModel): # type: ignore[misc]
|
|
549
553
|
An OpenTimeSeries object
|
550
554
|
|
551
555
|
"""
|
552
|
-
arr = array(self.values) / divider
|
556
|
+
arr: NDArray[float64] = array(self.values) / divider
|
553
557
|
|
554
558
|
deltas = array([i.days for i in self.tsdf.index[1:] - self.tsdf.index[:-1]])
|
555
|
-
|
556
|
-
|
557
|
-
|
559
|
+
arr = cast(
|
560
|
+
"NDArray[float64]",
|
561
|
+
cumprod(
|
562
|
+
a=insert(
|
563
|
+
arr=1.0 + deltas * arr[:-1] / days_in_year, obj=0, values=1.0
|
564
|
+
),
|
565
|
+
),
|
558
566
|
)
|
559
567
|
|
560
568
|
self.dates = [d.strftime("%Y-%m-%d") for d in self.tsdf.index]
|
@@ -672,32 +680,37 @@ class OpenTimeSeries(_CommonModel): # type: ignore[misc]
|
|
672
680
|
|
673
681
|
"""
|
674
682
|
earlier, later = self.calc_range(
|
675
|
-
months_offset=months_from_last,
|
683
|
+
months_offset=months_from_last,
|
684
|
+
from_dt=from_date,
|
685
|
+
to_dt=to_date,
|
676
686
|
)
|
677
687
|
if periods_in_a_year_fixed:
|
678
688
|
time_factor = float(periods_in_a_year_fixed)
|
679
689
|
else:
|
680
|
-
how_many =
|
681
|
-
cast("
|
682
|
-
|
683
|
-
|
690
|
+
how_many = (
|
691
|
+
self.tsdf.loc[cast("Timestamp", earlier) : cast("Timestamp", later)]
|
692
|
+
.count()
|
693
|
+
.iloc[0]
|
694
|
+
)
|
684
695
|
fraction = (later - earlier).days / 365.25
|
685
|
-
time_factor =
|
696
|
+
time_factor = how_many / fraction
|
686
697
|
|
687
|
-
data = self.tsdf.loc[
|
698
|
+
data = self.tsdf.loc[
|
699
|
+
cast("Timestamp", earlier) : cast("Timestamp", later)
|
700
|
+
].copy()
|
688
701
|
|
689
702
|
data[self.label, ValueType.RTRN] = log(
|
690
|
-
data.loc[:, self.tsdf.columns.to_numpy()[0]]
|
703
|
+
data.loc[:, self.tsdf.columns.to_numpy()[0]],
|
691
704
|
).diff()
|
692
705
|
|
693
706
|
rawdata = [
|
694
|
-
data
|
707
|
+
data[(self.label, ValueType.RTRN)]
|
695
708
|
.iloc[1:day_chunk]
|
696
709
|
.std(ddof=dlta_degr_freedms)
|
697
710
|
* sqrt(time_factor),
|
698
711
|
]
|
699
712
|
|
700
|
-
for item in data
|
713
|
+
for item in data[(self.label, ValueType.RTRN)].iloc[1:]:
|
701
714
|
prev = rawdata[-1]
|
702
715
|
rawdata.append(
|
703
716
|
sqrt(
|
@@ -865,6 +878,7 @@ def timeseries_chain(
|
|
865
878
|
|
866
879
|
dates.extend([x.strftime("%Y-%m-%d") for x in new.tsdf.index])
|
867
880
|
|
881
|
+
# noinspection PyTypeChecker
|
868
882
|
return back.__class__(
|
869
883
|
timeseries_id=new.timeseries_id,
|
870
884
|
instrument_id=new.instrument_id,
|
openseries/simulation.py
CHANGED
@@ -60,7 +60,7 @@ def _random_generator(seed: int | None) -> Generator:
|
|
60
60
|
return Generator(bit_generator=bg)
|
61
61
|
|
62
62
|
|
63
|
-
class ReturnSimulation(BaseModel):
|
63
|
+
class ReturnSimulation(BaseModel):
|
64
64
|
"""The class ReturnSimulation allows for simulating financial timeseries.
|
65
65
|
|
66
66
|
Parameters
|
@@ -115,7 +115,7 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
|
|
115
115
|
Simulation data
|
116
116
|
|
117
117
|
"""
|
118
|
-
return self.dframe.add(1.0).cumprod(axis="columns").T
|
118
|
+
return self.dframe.add(1.0).cumprod(axis="columns").T
|
119
119
|
|
120
120
|
@property
|
121
121
|
def realized_mean_return(self: Self) -> float:
|
@@ -463,7 +463,7 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
|
|
463
463
|
[ValueType.RTRN],
|
464
464
|
],
|
465
465
|
)
|
466
|
-
return sdf
|
466
|
+
return sdf
|
467
467
|
|
468
468
|
fdf = DataFrame()
|
469
469
|
for item in range(self.number_of_sims):
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: openseries
|
3
|
-
Version: 1.9.
|
3
|
+
Version: 1.9.6
|
4
4
|
Summary: Tools for analyzing financial timeseries.
|
5
5
|
License: # BSD 3-Clause License
|
6
6
|
|
@@ -29,6 +29,7 @@ License: # BSD 3-Clause License
|
|
29
29
|
however caused and on any theory of liability, whether in contract, strict liability,
|
30
30
|
or tort (including negligence or otherwise) arising in any way out of the use of this
|
31
31
|
software, even if advised of the possibility of such damage.
|
32
|
+
License-File: LICENSE.md
|
32
33
|
Keywords: python,finance,fintech,data-science,timeseries,timeseries-data,timeseries-analysis,investment,investment-analysis,investing
|
33
34
|
Author: Martin Karrin
|
34
35
|
Author-email: martin.karrin@captor.se
|
@@ -119,7 +120,7 @@ _,_=series.plot_series()
|
|
119
120
|
|
120
121
|
### Sample output using the report_html() function:
|
121
122
|
|
122
|
-
<img src="https://raw.githubusercontent.com/CaptorAB/openseries/master/
|
123
|
+
<img src="https://raw.githubusercontent.com/CaptorAB/openseries/master/openseries_plot.png" alt="Two Assets Compared" width="1000" />
|
123
124
|
|
124
125
|
## Development Instructions
|
125
126
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
openseries/__init__.py,sha256=WAh79oE-ceGG_yl4nBukkp3UPvmLk4u_GySL2xOKbxE,1375
|
2
|
+
openseries/_common_model.py,sha256=oZDTfUiQ4OG0T0RsBwTwRPHQcFpdOv1X7447hssoma4,82768
|
3
|
+
openseries/_risk.py,sha256=8XKZWWXrECo0Vd9r2kbcn4dzyPuo93DAEO8eSkv4w20,2357
|
4
|
+
openseries/datefixer.py,sha256=eVhxaFj_la_XZQuPQHvinTWEzCCn8ct_AnZEYPOpY6U,15775
|
5
|
+
openseries/frame.py,sha256=BG_Qnp0PMIZ7ZiShqTjO3Koj7Gs04n4WyzApCvtcUQY,57953
|
6
|
+
openseries/load_plotly.py,sha256=C6iQyabfi5ubSONuis3yRHb3bUktBtTDlovsDIaeHNQ,2266
|
7
|
+
openseries/owntypes.py,sha256=4IZvwl_YtoUZKlmVX65j5fp1zmfmOJLvaX8vxEIzIAY,9665
|
8
|
+
openseries/plotly_captor_logo.json,sha256=F5nhMzEyxKywtjvQqMTKgKRCJQYMDIiBgDSxdte8Clo,178
|
9
|
+
openseries/plotly_layouts.json,sha256=MvDEQuiqIhMBXBelXb1sedTOlTPheizv6NZRLeE9YS4,1431
|
10
|
+
openseries/portfoliotools.py,sha256=NqSTMlVv9Szu2usXtYzt__661VJoLsAf059ThfBr99Q,19677
|
11
|
+
openseries/report.py,sha256=pnxiEfbbkDmzj-lPcWgmYao1vtv1DUG8b73QDQKJa8o,14379
|
12
|
+
openseries/series.py,sha256=NgEZ2YPiLG55Bba48XS2zSh5dRLqz8hyGm-CGG1jNmY,29122
|
13
|
+
openseries/simulation.py,sha256=8J_iDlKjDVDiHMTWQAzHdXWxNR81pOz2RfdfUMHKSqQ,14337
|
14
|
+
openseries-1.9.6.dist-info/METADATA,sha256=GrNCO5aUnS5cuboqOsyU8sTN1YyHARF3RhFT1keNM3s,48324
|
15
|
+
openseries-1.9.6.dist-info/WHEEL,sha256=M5asmiAlL6HEcOq52Yi5mmk9KmTVjY2RDPtO4p9DMrc,88
|
16
|
+
openseries-1.9.6.dist-info/licenses/LICENSE.md,sha256=wNupG-KLsG0aTncb_SMNDh1ExtrKXlpxSJ6RC-g-SWs,1516
|
17
|
+
openseries-1.9.6.dist-info/RECORD,,
|