openseries 1.9.3__py3-none-any.whl → 1.9.5__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 +56 -28
- openseries/datefixer.py +4 -4
- openseries/frame.py +183 -165
- openseries/owntypes.py +4 -0
- openseries/plotly_layouts.json +1 -1
- openseries/portfoliotools.py +9 -10
- openseries/report.py +61 -56
- openseries/series.py +37 -23
- openseries/simulation.py +9 -6
- {openseries-1.9.3.dist-info → openseries-1.9.5.dist-info}/METADATA +1 -1
- openseries-1.9.5.dist-info/RECORD +17 -0
- openseries-1.9.3.dist-info/RECORD +0 -17
- {openseries-1.9.3.dist-info → openseries-1.9.5.dist-info}/LICENSE.md +0 -0
- {openseries-1.9.3.dist-info → openseries-1.9.5.dist-info}/WHEEL +0 -0
openseries/owntypes.py
CHANGED
@@ -376,3 +376,7 @@ class IncorrectArgumentComboError(Exception):
|
|
376
376
|
|
377
377
|
class PropertiesInputValidationError(Exception):
|
378
378
|
"""Raised when duplicate strings are provided."""
|
379
|
+
|
380
|
+
|
381
|
+
class ResampleDataLossError(Exception):
|
382
|
+
"""Raised when user attempts to run resample_to_business_period_ends on returns."""
|
openseries/plotly_layouts.json
CHANGED
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,
|
@@ -320,7 +319,7 @@ def efficient_frontier(
|
|
320
319
|
|
321
320
|
if tweak:
|
322
321
|
limit_tweak = 0.001
|
323
|
-
line_df["stdev_diff"] = line_df.stdev.pct_change()
|
322
|
+
line_df["stdev_diff"] = line_df.stdev.ffill().pct_change()
|
324
323
|
line_df = line_df.loc[line_df.stdev_diff.abs() > limit_tweak]
|
325
324
|
line_df = line_df.drop(columns="stdev_diff")
|
326
325
|
|
@@ -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
|
@@ -441,7 +440,7 @@ 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
|
443
|
+
vol = cast("Series[float]", assets.vol)
|
445
444
|
plotframe = DataFrame(
|
446
445
|
data=[
|
447
446
|
assets.arithmetic_ret,
|
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
|
|
@@ -68,21 +68,19 @@ def calendar_period_returns(
|
|
68
68
|
"""
|
69
69
|
copied = data.from_deepcopy()
|
70
70
|
copied.resample_to_business_period_ends(freq=freq)
|
71
|
-
|
72
|
-
if not any(vtypes):
|
73
|
-
copied.value_to_ret()
|
71
|
+
copied.value_to_ret()
|
74
72
|
cldr = copied.tsdf.iloc[1:].copy()
|
75
73
|
if relabel:
|
76
74
|
if freq.upper() == "BYE":
|
77
|
-
cldr.index = [d.year for d in cldr.index]
|
75
|
+
cldr.index = Index([d.year for d in cldr.index])
|
78
76
|
elif freq.upper() == "BQE":
|
79
|
-
cldr.index =
|
80
|
-
Timestamp(d).to_period("Q").strftime("Q%q %Y") for d in cldr.index
|
81
|
-
|
77
|
+
cldr.index = Index(
|
78
|
+
[Timestamp(d).to_period("Q").strftime("Q%q %Y") for d in cldr.index]
|
79
|
+
)
|
82
80
|
else:
|
83
|
-
cldr.index = [d.strftime("%b %y") for d in cldr.index]
|
81
|
+
cldr.index = Index([d.strftime("%b %y") for d in cldr.index])
|
84
82
|
|
85
|
-
return cldr
|
83
|
+
return cldr
|
86
84
|
|
87
85
|
|
88
86
|
def report_html(
|
@@ -129,9 +127,10 @@ def report_html(
|
|
129
127
|
Plotly Figure and a div section or a html filename with location
|
130
128
|
|
131
129
|
"""
|
132
|
-
data.
|
130
|
+
copied = data.from_deepcopy()
|
131
|
+
copied.trunc_frame().value_nan_handle().to_cumret()
|
133
132
|
|
134
|
-
if
|
133
|
+
if copied.yearfrac > 1.0:
|
135
134
|
properties = [
|
136
135
|
"geo_ret",
|
137
136
|
"vol",
|
@@ -219,10 +218,10 @@ def report_html(
|
|
219
218
|
],
|
220
219
|
)
|
221
220
|
|
222
|
-
for item, lbl in enumerate(
|
221
|
+
for item, lbl in enumerate(copied.columns_lvl_zero):
|
223
222
|
figure.add_scatter(
|
224
|
-
x=
|
225
|
-
y=
|
223
|
+
x=copied.tsdf.index,
|
224
|
+
y=copied.tsdf.iloc[:, item],
|
226
225
|
hovertemplate="%{y:.2%}<br>%{x|%Y-%m-%d}",
|
227
226
|
line={"width": 2.5, "dash": "solid"},
|
228
227
|
mode="lines",
|
@@ -233,18 +232,19 @@ def report_html(
|
|
233
232
|
)
|
234
233
|
|
235
234
|
quarter_of_year = 0.25
|
236
|
-
if
|
237
|
-
tmp =
|
235
|
+
if copied.yearfrac < quarter_of_year:
|
236
|
+
tmp = copied.from_deepcopy()
|
238
237
|
bdf = tmp.value_to_ret().tsdf.iloc[1:]
|
239
238
|
else:
|
240
|
-
bdf = calendar_period_returns(data, freq=bar_freq)
|
239
|
+
bdf = calendar_period_returns(data=copied, freq=bar_freq)
|
241
240
|
|
242
|
-
for item in range(
|
241
|
+
for item in range(copied.item_count):
|
242
|
+
col_name = cast("tuple[str, ValueType]", bdf.iloc[:, item].name)
|
243
243
|
figure.add_bar(
|
244
244
|
x=bdf.index,
|
245
245
|
y=bdf.iloc[:, item],
|
246
246
|
hovertemplate="%{y:.2%}<br>%{x}",
|
247
|
-
name=
|
247
|
+
name=col_name[0],
|
248
248
|
showlegend=False,
|
249
249
|
row=2,
|
250
250
|
col=1,
|
@@ -265,8 +265,10 @@ def report_html(
|
|
265
265
|
]
|
266
266
|
|
267
267
|
# noinspection PyTypeChecker
|
268
|
-
rpt_df =
|
269
|
-
|
268
|
+
rpt_df = copied.all_properties(
|
269
|
+
properties=cast("list[LiteralFrameProps]", properties)
|
270
|
+
)
|
271
|
+
alpha_frame = copied.from_deepcopy()
|
270
272
|
alpha_frame.to_cumret()
|
271
273
|
with catch_warnings():
|
272
274
|
simplefilter("ignore")
|
@@ -279,14 +281,16 @@ def report_html(
|
|
279
281
|
for aname in alpha_frame.columns_lvl_zero[:-1]
|
280
282
|
]
|
281
283
|
alphas.append("")
|
282
|
-
ar = DataFrame(
|
284
|
+
ar = DataFrame(
|
285
|
+
data=alphas, index=copied.tsdf.columns, columns=["Jensen's Alpha"]
|
286
|
+
).T
|
283
287
|
rpt_df = concat([rpt_df, ar])
|
284
|
-
ir =
|
288
|
+
ir = copied.info_ratio_func()
|
285
289
|
ir.name = "Information Ratio"
|
286
290
|
ir.iloc[-1] = None
|
287
|
-
|
288
|
-
rpt_df = concat([rpt_df,
|
289
|
-
te_frame =
|
291
|
+
ir_df = ir.to_frame().T
|
292
|
+
rpt_df = concat([rpt_df, ir_df])
|
293
|
+
te_frame = copied.from_deepcopy()
|
290
294
|
te_frame.resample("7D")
|
291
295
|
with catch_warnings():
|
292
296
|
simplefilter("ignore")
|
@@ -300,11 +304,11 @@ def report_html(
|
|
300
304
|
else:
|
301
305
|
te.iloc[-1] = None
|
302
306
|
te.name = "Tracking Error (weekly)"
|
303
|
-
|
304
|
-
rpt_df = concat([rpt_df,
|
307
|
+
te_df = te.to_frame().T
|
308
|
+
rpt_df = concat([rpt_df, te_df])
|
305
309
|
|
306
|
-
if
|
307
|
-
crm =
|
310
|
+
if copied.yearfrac > 1.0:
|
311
|
+
crm = copied.from_deepcopy()
|
308
312
|
crm.resample("ME")
|
309
313
|
cru_save = Series(
|
310
314
|
data=[""] * crm.item_count,
|
@@ -324,10 +328,10 @@ def report_html(
|
|
324
328
|
else:
|
325
329
|
cru.iloc[-1] = None
|
326
330
|
cru.name = "Capture Ratio (monthly)"
|
327
|
-
|
328
|
-
rpt_df = concat([rpt_df,
|
331
|
+
cru_df = cru.to_frame().T
|
332
|
+
rpt_df = concat([rpt_df, cru_df])
|
329
333
|
formats.append("{:.2f}")
|
330
|
-
beta_frame =
|
334
|
+
beta_frame = copied.from_deepcopy()
|
331
335
|
beta_frame.resample("7D").value_nan_handle("drop")
|
332
336
|
beta_frame.to_cumret()
|
333
337
|
betas: list[str | float] = [
|
@@ -337,51 +341,50 @@ def report_html(
|
|
337
341
|
)
|
338
342
|
for bname in beta_frame.columns_lvl_zero[:-1]
|
339
343
|
]
|
340
|
-
# noinspection PyTypeChecker
|
341
344
|
betas.append("")
|
342
345
|
br = DataFrame(
|
343
346
|
data=betas,
|
344
|
-
index=
|
347
|
+
index=copied.tsdf.columns,
|
345
348
|
columns=["Index Beta (weekly)"],
|
346
349
|
).T
|
347
350
|
rpt_df = concat([rpt_df, br])
|
348
351
|
|
349
352
|
for item, f in zip(rpt_df.index, formats, strict=False):
|
350
353
|
rpt_df.loc[item] = rpt_df.loc[item].apply(
|
351
|
-
lambda x, fmt=f: x if (isinstance(x, str) or x is None) else fmt.format(x),
|
354
|
+
lambda x, fmt=f: x if (isinstance(x, str) or x is None) else fmt.format(x),
|
352
355
|
)
|
353
356
|
|
354
|
-
rpt_df.index = labels_init
|
357
|
+
rpt_df.index = Index(labels_init)
|
355
358
|
|
356
|
-
this_year =
|
357
|
-
this_month =
|
358
|
-
ytd = cast("Series[float]",
|
359
|
+
this_year = copied.last_idx.year
|
360
|
+
this_month = copied.last_idx.month
|
361
|
+
ytd = cast("Series[float]", copied.value_ret_calendar_period(year=this_year)).map(
|
359
362
|
"{:.2%}".format
|
360
363
|
)
|
361
364
|
ytd.name = "Year-to-Date"
|
362
365
|
mtd = cast(
|
363
366
|
"Series[float]",
|
364
|
-
|
367
|
+
copied.value_ret_calendar_period(year=this_year, month=this_month),
|
365
368
|
).map(
|
366
369
|
"{:.2%}".format,
|
367
370
|
)
|
368
371
|
mtd.name = "Month-to-Date"
|
369
|
-
|
370
|
-
|
371
|
-
rpt_df = concat([rpt_df,
|
372
|
-
rpt_df = concat([rpt_df,
|
372
|
+
ytd_df = ytd.to_frame().T
|
373
|
+
mtd_df = mtd.to_frame().T
|
374
|
+
rpt_df = concat([rpt_df, ytd_df])
|
375
|
+
rpt_df = concat([rpt_df, mtd_df])
|
373
376
|
rpt_df = rpt_df.reindex(labels_final)
|
374
377
|
|
375
|
-
rpt_df.index = [f"<b>{x}</b>" for x in rpt_df.index]
|
378
|
+
rpt_df.index = Index([f"<b>{x}</b>" for x in rpt_df.index])
|
376
379
|
rpt_df = rpt_df.reset_index()
|
377
380
|
|
378
|
-
colmns = ["", *
|
381
|
+
colmns = ["", *copied.columns_lvl_zero]
|
379
382
|
columns = [f"<b>{x}</b>" for x in colmns]
|
380
383
|
aligning = ["left"] + ["center"] * (len(columns) - 1)
|
381
384
|
|
382
385
|
col_even_color = "lightgrey"
|
383
386
|
col_odd_color = "white"
|
384
|
-
color_lst = ["grey"] + [col_odd_color] * (
|
387
|
+
color_lst = ["grey"] + [col_odd_color] * (copied.item_count - 1) + [col_even_color]
|
385
388
|
|
386
389
|
tablevalues = rpt_df.transpose().to_numpy().tolist()
|
387
390
|
cleanedtablevalues = list(tablevalues)[:-1]
|
@@ -426,7 +429,9 @@ def report_html(
|
|
426
429
|
figure.add_layout_image(logo)
|
427
430
|
|
428
431
|
figure.update_layout(fig.get("layout"))
|
429
|
-
colorway: list[str] = cast("dict[str, list[str]]", fig["layout"]).get(
|
432
|
+
colorway: list[str] = cast("dict[str, list[str]]", fig["layout"]).get(
|
433
|
+
"colorway", []
|
434
|
+
)
|
430
435
|
|
431
436
|
if vertical_legend:
|
432
437
|
legend = {
|
@@ -447,7 +452,7 @@ def report_html(
|
|
447
452
|
|
448
453
|
figure.update_layout(
|
449
454
|
legend=legend,
|
450
|
-
colorway=colorway[:
|
455
|
+
colorway=colorway[: copied.item_count],
|
451
456
|
)
|
452
457
|
figure.update_xaxes(gridcolor="#EEEEEE", automargin=True, tickangle=-45)
|
453
458
|
figure.update_yaxes(tickformat=".2%", gridcolor="#EEEEEE", automargin=True)
|
openseries/series.py
CHANGED
@@ -9,13 +9,13 @@ 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
19
|
|
20
20
|
from numpy import (
|
21
21
|
append,
|
@@ -53,11 +53,15 @@ from .owntypes import (
|
|
53
53
|
LiteralSeriesProps,
|
54
54
|
MarketsNotStringNorListStrError,
|
55
55
|
OpenTimeSeriesPropertiesList,
|
56
|
+
ResampleDataLossError,
|
56
57
|
Self,
|
57
58
|
ValueListType,
|
58
59
|
ValueType,
|
59
60
|
)
|
60
61
|
|
62
|
+
FieldValidator = cast("Callable[..., Callable[..., Any]]", field_validator)
|
63
|
+
ModelValidator = cast("Callable[..., Callable[..., Any]]", model_validator)
|
64
|
+
|
61
65
|
logger = getLogger(__name__)
|
62
66
|
|
63
67
|
__all__ = ["OpenTimeSeries", "timeseries_chain"]
|
@@ -122,21 +126,21 @@ class OpenTimeSeries(_CommonModel):
|
|
122
126
|
isin: str | None = None
|
123
127
|
label: str | None = None
|
124
128
|
|
125
|
-
@
|
129
|
+
@FieldValidator("domestic", mode="before")
|
126
130
|
@classmethod
|
127
131
|
def _validate_domestic(cls, value: CurrencyStringType) -> CurrencyStringType:
|
128
132
|
"""Pydantic validator to ensure domestic field is validated."""
|
129
133
|
_ = Currency(ccy=value)
|
130
134
|
return value
|
131
135
|
|
132
|
-
@
|
136
|
+
@FieldValidator("countries", mode="before")
|
133
137
|
@classmethod
|
134
138
|
def _validate_countries(cls, value: CountriesType) -> CountriesType:
|
135
139
|
"""Pydantic validator to ensure countries field is validated."""
|
136
140
|
_ = Countries(countryinput=value)
|
137
141
|
return value
|
138
142
|
|
139
|
-
@
|
143
|
+
@FieldValidator("markets", mode="before")
|
140
144
|
@classmethod
|
141
145
|
def _validate_markets(
|
142
146
|
cls, value: list[str] | str | None
|
@@ -155,7 +159,7 @@ class OpenTimeSeries(_CommonModel):
|
|
155
159
|
raise MarketsNotStringNorListStrError(item_msg)
|
156
160
|
raise MarketsNotStringNorListStrError(msg)
|
157
161
|
|
158
|
-
@
|
162
|
+
@ModelValidator(mode="after")
|
159
163
|
def _dates_and_values_validate(self: Self) -> Self:
|
160
164
|
"""Pydantic validator to ensure dates and values are validated."""
|
161
165
|
values_list_length = len(self.values)
|
@@ -369,19 +373,17 @@ class OpenTimeSeries(_CommonModel):
|
|
369
373
|
An OpenTimeSeries object
|
370
374
|
|
371
375
|
"""
|
372
|
-
if
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
deltas = array(
|
381
|
-
[i.days for i in DatetimeIndex(d_range)[1:] - DatetimeIndex(d_range)[:-1]], # type: ignore[arg-type]
|
382
|
-
)
|
376
|
+
if d_range is None:
|
377
|
+
if days is not None and end_dt is not None:
|
378
|
+
d_range = DatetimeIndex(
|
379
|
+
[d.date() for d in date_range(periods=days, end=end_dt, freq="D")],
|
380
|
+
)
|
381
|
+
else:
|
382
|
+
msg = "If d_range is not provided both days and end_dt must be."
|
383
|
+
raise IncorrectArgumentComboError(msg)
|
384
|
+
deltas = array([i.days for i in d_range[1:] - d_range[:-1]])
|
383
385
|
arr: list[float] = list(cumprod(insert(1 + deltas * rate / 365, 0, 1.0)))
|
384
|
-
dates = [d.strftime("%Y-%m-%d") for d in
|
386
|
+
dates = [d.strftime("%Y-%m-%d") for d in d_range]
|
385
387
|
|
386
388
|
return cls(
|
387
389
|
timeseries_id="",
|
@@ -468,7 +470,7 @@ class OpenTimeSeries(_CommonModel):
|
|
468
470
|
The returns of the values in the series
|
469
471
|
|
470
472
|
"""
|
471
|
-
returns = self.tsdf.pct_change()
|
473
|
+
returns = self.tsdf.ffill().pct_change()
|
472
474
|
returns.iloc[0] = 0
|
473
475
|
self.valuetype = ValueType.RTRN
|
474
476
|
arrays = [[self.label], [self.valuetype]]
|
@@ -551,8 +553,7 @@ class OpenTimeSeries(_CommonModel):
|
|
551
553
|
arr = array(self.values) / divider
|
552
554
|
|
553
555
|
deltas = array([i.days for i in self.tsdf.index[1:] - self.tsdf.index[:-1]])
|
554
|
-
|
555
|
-
arr = cumprod( # type: ignore[assignment]
|
556
|
+
arr = cumprod(
|
556
557
|
a=insert(arr=1.0 + deltas * arr[:-1] / days_in_year, obj=0, values=1.0)
|
557
558
|
)
|
558
559
|
|
@@ -586,7 +587,10 @@ class OpenTimeSeries(_CommonModel):
|
|
586
587
|
|
587
588
|
"""
|
588
589
|
self.tsdf.index = DatetimeIndex(self.tsdf.index)
|
589
|
-
self.
|
590
|
+
if self.valuetype == ValueType.RTRN:
|
591
|
+
self.tsdf = self.tsdf.resample(freq).sum()
|
592
|
+
else:
|
593
|
+
self.tsdf = self.tsdf.resample(freq).last()
|
590
594
|
self.tsdf.index = Index(d.date() for d in DatetimeIndex(self.tsdf.index))
|
591
595
|
return self
|
592
596
|
|
@@ -612,6 +616,14 @@ class OpenTimeSeries(_CommonModel):
|
|
612
616
|
An OpenTimeSeries object
|
613
617
|
|
614
618
|
"""
|
619
|
+
if self.valuetype == ValueType.RTRN:
|
620
|
+
msg = (
|
621
|
+
"Do not run resample_to_business_period_ends on return series. "
|
622
|
+
"The operation will pick the last data point in the sparser series. "
|
623
|
+
"It will not sum returns and therefore data will be lost."
|
624
|
+
)
|
625
|
+
raise ResampleDataLossError(msg)
|
626
|
+
|
615
627
|
dates = _do_resample_to_business_period_ends(
|
616
628
|
data=self.tsdf,
|
617
629
|
freq=freq,
|
@@ -659,7 +671,9 @@ class OpenTimeSeries(_CommonModel):
|
|
659
671
|
Series EWMA volatility
|
660
672
|
|
661
673
|
"""
|
662
|
-
earlier, later = self.calc_range(
|
674
|
+
earlier, later = self.calc_range(
|
675
|
+
months_offset=months_from_last, from_dt=from_date, to_dt=to_date
|
676
|
+
)
|
663
677
|
if periods_in_a_year_fixed:
|
664
678
|
time_factor = float(periods_in_a_year_fixed)
|
665
679
|
else:
|
@@ -725,7 +739,7 @@ class OpenTimeSeries(_CommonModel):
|
|
725
739
|
returns_input = True
|
726
740
|
else:
|
727
741
|
values = [cast("float", self.tsdf.iloc[0, 0])]
|
728
|
-
ra_df = self.tsdf.pct_change()
|
742
|
+
ra_df = self.tsdf.ffill().pct_change()
|
729
743
|
returns_input = False
|
730
744
|
ra_df = ra_df.dropna()
|
731
745
|
|
openseries/simulation.py
CHANGED
@@ -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:
|
@@ -129,7 +129,9 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
|
|
129
129
|
"""
|
130
130
|
return cast(
|
131
131
|
"float",
|
132
|
-
(
|
132
|
+
(
|
133
|
+
self.results.ffill().pct_change().mean() * self.trading_days_in_year
|
134
|
+
).iloc[0],
|
133
135
|
)
|
134
136
|
|
135
137
|
@property
|
@@ -144,9 +146,10 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
|
|
144
146
|
"""
|
145
147
|
return cast(
|
146
148
|
"float",
|
147
|
-
(
|
148
|
-
|
149
|
-
|
149
|
+
(
|
150
|
+
self.results.ffill().pct_change().std()
|
151
|
+
* sqrt(self.trading_days_in_year)
|
152
|
+
).iloc[0],
|
150
153
|
)
|
151
154
|
|
152
155
|
@classmethod
|
@@ -460,7 +463,7 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
|
|
460
463
|
[ValueType.RTRN],
|
461
464
|
],
|
462
465
|
)
|
463
|
-
return sdf
|
466
|
+
return sdf
|
464
467
|
|
465
468
|
fdf = DataFrame()
|
466
469
|
for item in range(self.number_of_sims):
|
@@ -0,0 +1,17 @@
|
|
1
|
+
openseries/__init__.py,sha256=WAh79oE-ceGG_yl4nBukkp3UPvmLk4u_GySL2xOKbxE,1375
|
2
|
+
openseries/_common_model.py,sha256=Nug9DIp54q7tt0yHFEQKAZPrG9c1Oy6VpyqoRWKOC4I,85499
|
3
|
+
openseries/_risk.py,sha256=8XKZWWXrECo0Vd9r2kbcn4dzyPuo93DAEO8eSkv4w20,2357
|
4
|
+
openseries/datefixer.py,sha256=Z3AKLvULzy9MPQOndKhay0nGx2EgYcjVFNjT9qReoHk,15727
|
5
|
+
openseries/frame.py,sha256=a3TPLdvapnvHU_wbhPO0G95UHaHlJLXnsSmm-Ti_2sw,58579
|
6
|
+
openseries/load_plotly.py,sha256=C6iQyabfi5ubSONuis3yRHb3bUktBtTDlovsDIaeHNQ,2266
|
7
|
+
openseries/owntypes.py,sha256=P9CKoLtjUaFiktLb_axihrlVR5bJfdDbSFJC72kQG2o,9584
|
8
|
+
openseries/plotly_captor_logo.json,sha256=F5nhMzEyxKywtjvQqMTKgKRCJQYMDIiBgDSxdte8Clo,178
|
9
|
+
openseries/plotly_layouts.json,sha256=MvDEQuiqIhMBXBelXb1sedTOlTPheizv6NZRLeE9YS4,1431
|
10
|
+
openseries/portfoliotools.py,sha256=NMSp-dYjPRjBJpZ9W20IKDrQmBDk7e4qkTPT4QFx6io,19721
|
11
|
+
openseries/report.py,sha256=iWe68o883EIU9B_t-61fl41wzTY2e6p_ZHgST2uoH3g,14393
|
12
|
+
openseries/series.py,sha256=HT2U_gUhiZMhvt7hzpUPakKBEXI64Ur2WqoaUhXV11k,28925
|
13
|
+
openseries/simulation.py,sha256=t2LFlAT9lcfPqqGEXOUoEgIG2gDEuGps3Qd3IgN_GLk,14359
|
14
|
+
openseries-1.9.5.dist-info/LICENSE.md,sha256=wNupG-KLsG0aTncb_SMNDh1ExtrKXlpxSJ6RC-g-SWs,1516
|
15
|
+
openseries-1.9.5.dist-info/METADATA,sha256=XYz8k9ujPY_MuwGjyHEyxyCgjKppszQJfmFq3tZ67U4,48301
|
16
|
+
openseries-1.9.5.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
17
|
+
openseries-1.9.5.dist-info/RECORD,,
|
@@ -1,17 +0,0 @@
|
|
1
|
-
openseries/__init__.py,sha256=WAh79oE-ceGG_yl4nBukkp3UPvmLk4u_GySL2xOKbxE,1375
|
2
|
-
openseries/_common_model.py,sha256=ydS3CNqxtd5GPa9R-28ApcgjKe1d9vXPyaF2aECkv1A,84720
|
3
|
-
openseries/_risk.py,sha256=8XKZWWXrECo0Vd9r2kbcn4dzyPuo93DAEO8eSkv4w20,2357
|
4
|
-
openseries/datefixer.py,sha256=FBe0zEcCDbrMPatN9OvaKqXJd9EQOqXzknQQ1N3Ji0s,15774
|
5
|
-
openseries/frame.py,sha256=AAIHHvs0f1HoRtlXQZmF8PPFPNa134KUYaKnSVad-xY,57752
|
6
|
-
openseries/load_plotly.py,sha256=C6iQyabfi5ubSONuis3yRHb3bUktBtTDlovsDIaeHNQ,2266
|
7
|
-
openseries/owntypes.py,sha256=VrOTslL0H8hM0BL-E57inlgfV_JEto9WMH9RCE2G0Ug,9454
|
8
|
-
openseries/plotly_captor_logo.json,sha256=F5nhMzEyxKywtjvQqMTKgKRCJQYMDIiBgDSxdte8Clo,178
|
9
|
-
openseries/plotly_layouts.json,sha256=9tKAeittrjwJWhBMV8SnCDAWdhgbVnUqXcN6P_J_bos,1433
|
10
|
-
openseries/portfoliotools.py,sha256=X0byXAfLI8AOc0kWIa1edJujgTRhSP4BNLQgbE28YPk,19667
|
11
|
-
openseries/report.py,sha256=s81bPGlgyzBdWR_MZTpb9twj2N9uxtnZZrV7FRpFyNU,14238
|
12
|
-
openseries/series.py,sha256=sk6ZeFEsM9sET52y8Zzgoo7zBZvEhcYkddyIMa5Kp4s,28506
|
13
|
-
openseries/simulation.py,sha256=fc0zonz9aogRIT7p06CBOUm6-mArVIg9UJF3bdgq3CE,14359
|
14
|
-
openseries-1.9.3.dist-info/LICENSE.md,sha256=wNupG-KLsG0aTncb_SMNDh1ExtrKXlpxSJ6RC-g-SWs,1516
|
15
|
-
openseries-1.9.3.dist-info/METADATA,sha256=jQA3_jxZVvwOcCxBrDXrD4ZpnqpZkO9UyFbdPr791Z0,48301
|
16
|
-
openseries-1.9.3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
17
|
-
openseries-1.9.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|