openseries 1.8.0__py3-none-any.whl → 1.8.2__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 +2 -1
- openseries/_common_model.py +180 -155
- openseries/_risk.py +4 -4
- openseries/datefixer.py +20 -14
- openseries/frame.py +124 -109
- openseries/load_plotly.py +6 -4
- openseries/owntypes.py +66 -5
- openseries/plotly_layouts.json +6 -6
- openseries/portfoliotools.py +33 -29
- openseries/series.py +69 -79
- openseries/simulation.py +15 -14
- openseries-1.8.2.dist-info/LICENSE.md +27 -0
- {openseries-1.8.0.dist-info → openseries-1.8.2.dist-info}/METADATA +44 -16
- openseries-1.8.2.dist-info/RECORD +16 -0
- {openseries-1.8.0.dist-info → openseries-1.8.2.dist-info}/WHEEL +1 -1
- openseries-1.8.0.dist-info/LICENSE.md +0 -27
- openseries-1.8.0.dist-info/RECORD +0 -16
openseries/load_plotly.py
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
from json import load
|
6
|
-
from logging import
|
6
|
+
from logging import getLogger
|
7
7
|
from pathlib import Path
|
8
8
|
from typing import TYPE_CHECKING
|
9
9
|
|
@@ -13,6 +13,8 @@ from requests.exceptions import ConnectionError as RequestsConnectionError
|
|
13
13
|
if TYPE_CHECKING:
|
14
14
|
from .owntypes import CaptorLogoType, PlotlyLayoutType # pragma: no cover
|
15
15
|
|
16
|
+
logger = getLogger(__name__)
|
17
|
+
|
16
18
|
__all__ = ["load_plotly_dict"]
|
17
19
|
|
18
20
|
|
@@ -24,7 +26,7 @@ def _check_remote_file_existence(url: str) -> bool:
|
|
24
26
|
url: str
|
25
27
|
Path to remote file
|
26
28
|
|
27
|
-
Returns
|
29
|
+
Returns:
|
28
30
|
-------
|
29
31
|
bool
|
30
32
|
True if url is valid and False otherwise
|
@@ -52,7 +54,7 @@ def load_plotly_dict(
|
|
52
54
|
responsive : bool
|
53
55
|
Flag whether to load as responsive
|
54
56
|
|
55
|
-
Returns
|
57
|
+
Returns:
|
56
58
|
-------
|
57
59
|
tuple[PlotlyLayoutType, CaptorLogoType]
|
58
60
|
A dictionary with the Plotly config and layout template
|
@@ -69,7 +71,7 @@ def load_plotly_dict(
|
|
69
71
|
|
70
72
|
if _check_remote_file_existence(url=logo["source"]) is False:
|
71
73
|
msg = f"Failed to add logo image from URL {logo['source']}"
|
72
|
-
warning(msg)
|
74
|
+
logger.warning(msg)
|
73
75
|
logo = {}
|
74
76
|
|
75
77
|
fig["config"].update({"responsive": responsive})
|
openseries/owntypes.py
CHANGED
@@ -10,9 +10,14 @@ from typing import Annotated, ClassVar, Literal, Union
|
|
10
10
|
from numpy import datetime64
|
11
11
|
from pandas import Timestamp
|
12
12
|
from pydantic import BaseModel, Field, StringConstraints, conlist, conset
|
13
|
-
from typing_extensions import Self
|
14
13
|
|
15
|
-
|
14
|
+
try:
|
15
|
+
from typing import Self # type: ignore[attr-defined,unused-ignore]
|
16
|
+
except ImportError: # pragma: no cover
|
17
|
+
from typing_extensions import Self
|
18
|
+
|
19
|
+
|
20
|
+
__all__ = ["Self", "ValueType"]
|
16
21
|
|
17
22
|
|
18
23
|
CountryStringType = Annotated[
|
@@ -33,7 +38,7 @@ CountryListType = conset(
|
|
33
38
|
CountriesType = Union[CountryListType, CountryStringType] # type: ignore[valid-type] # noqa: UP007
|
34
39
|
|
35
40
|
|
36
|
-
class Countries(BaseModel):
|
41
|
+
class Countries(BaseModel): # type: ignore[misc]
|
37
42
|
"""Declare Countries."""
|
38
43
|
|
39
44
|
countryinput: CountriesType
|
@@ -52,7 +57,7 @@ CurrencyStringType = Annotated[
|
|
52
57
|
]
|
53
58
|
|
54
59
|
|
55
|
-
class Currency(BaseModel):
|
60
|
+
class Currency(BaseModel): # type: ignore[misc]
|
56
61
|
"""Declare Currency."""
|
57
62
|
|
58
63
|
ccy: CurrencyStringType
|
@@ -262,7 +267,7 @@ class PropertiesList(list[str]):
|
|
262
267
|
if len(duplicates) != 0:
|
263
268
|
msg += f"Duplicate string(s): {list(duplicates)}."
|
264
269
|
if len(msg) != 0:
|
265
|
-
raise
|
270
|
+
raise PropertiesInputValidationError(msg)
|
266
271
|
|
267
272
|
|
268
273
|
class OpenTimeSeriesPropertiesList(PropertiesList):
|
@@ -316,3 +321,59 @@ class ValueType(str, Enum):
|
|
316
321
|
ROLLRTRN = "Rolling returns"
|
317
322
|
ROLLVAR = "Rolling VaR"
|
318
323
|
ROLLVOL = "Rolling volatility"
|
324
|
+
|
325
|
+
|
326
|
+
class MixedValuetypesError(Exception):
|
327
|
+
"""Raised when provided timeseries valuetypes are not the same."""
|
328
|
+
|
329
|
+
|
330
|
+
class AtLeastOneFrameError(Exception):
|
331
|
+
"""Raised when none of the possible frame inputs is provided."""
|
332
|
+
|
333
|
+
|
334
|
+
class DateAlignmentError(Exception):
|
335
|
+
"""Raised when date input is not aligned with existing range."""
|
336
|
+
|
337
|
+
|
338
|
+
class NumberOfItemsAndLabelsNotSameError(Exception):
|
339
|
+
"""Raised when number of labels is not matching the number of timeseries."""
|
340
|
+
|
341
|
+
|
342
|
+
class InitialValueZeroError(Exception):
|
343
|
+
"""Raised when a calculation cannot be performed due to initial value(s) zero."""
|
344
|
+
|
345
|
+
|
346
|
+
class CountriesNotStringNorListStrError(Exception):
|
347
|
+
"""Raised when countries argument is not provided in correct format."""
|
348
|
+
|
349
|
+
|
350
|
+
class TradingDaysNotAboveZeroError(Exception):
|
351
|
+
"""Raised when trading days argument is not above zero."""
|
352
|
+
|
353
|
+
|
354
|
+
class BothStartAndEndError(Exception):
|
355
|
+
"""Raised when both start and end dates are provided."""
|
356
|
+
|
357
|
+
|
358
|
+
class NoWeightsError(Exception):
|
359
|
+
"""Raised when no weights are provided to function where necessary."""
|
360
|
+
|
361
|
+
|
362
|
+
class LabelsNotUniqueError(Exception):
|
363
|
+
"""Raised when provided label names are not unique."""
|
364
|
+
|
365
|
+
|
366
|
+
class RatioInputError(Exception):
|
367
|
+
"""Raised when ratio keyword not provided correctly."""
|
368
|
+
|
369
|
+
|
370
|
+
class MergingResultedInEmptyError(Exception):
|
371
|
+
"""Raised when a merge resulted in an empty DataFrame."""
|
372
|
+
|
373
|
+
|
374
|
+
class IncorrectArgumentComboError(Exception):
|
375
|
+
"""Raised when correct combination of arguments is not provided."""
|
376
|
+
|
377
|
+
|
378
|
+
class PropertiesInputValidationError(Exception):
|
379
|
+
"""Raised when duplicate strings are provided."""
|
openseries/plotly_layouts.json
CHANGED
@@ -43,25 +43,25 @@
|
|
43
43
|
"size": 14
|
44
44
|
},
|
45
45
|
"legend": {
|
46
|
-
"bgcolor": "rgba(
|
46
|
+
"bgcolor": "rgba(255,255,255,1)",
|
47
47
|
"orientation": "h",
|
48
48
|
"x": 0.98,
|
49
49
|
"xanchor": "right",
|
50
50
|
"y": -0.15,
|
51
51
|
"yanchor": "bottom"
|
52
52
|
},
|
53
|
-
"paper_bgcolor": "rgba(
|
54
|
-
"plot_bgcolor": "rgba(
|
53
|
+
"paper_bgcolor": "rgba(255,255,255,1)",
|
54
|
+
"plot_bgcolor": "rgba(255,255,255,1)",
|
55
55
|
"showlegend": true,
|
56
56
|
"title": {
|
57
|
+
"font": {
|
58
|
+
"size": 32
|
59
|
+
},
|
57
60
|
"x": 0.5,
|
58
61
|
"xanchor": "center",
|
59
62
|
"y": 0.95,
|
60
63
|
"yanchor": "top"
|
61
64
|
},
|
62
|
-
"titlefont": {
|
63
|
-
"size": 24
|
64
|
-
},
|
65
65
|
"xaxis": {
|
66
66
|
"gridcolor": "#EEEEEE",
|
67
67
|
"tickangle": -45,
|
openseries/portfoliotools.py
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
# mypy: disable-error-code="index,assignment"
|
4
4
|
from __future__ import annotations
|
5
5
|
|
6
|
-
from collections.abc import Callable
|
7
6
|
from inspect import stack
|
8
7
|
from pathlib import Path
|
9
8
|
from typing import TYPE_CHECKING, cast
|
@@ -22,23 +21,24 @@ from numpy import (
|
|
22
21
|
from numpy import (
|
23
22
|
sum as npsum,
|
24
23
|
)
|
25
|
-
from numpy.typing import NDArray
|
26
24
|
from pandas import (
|
27
25
|
DataFrame,
|
28
26
|
Series,
|
29
27
|
concat,
|
30
28
|
)
|
31
|
-
from plotly.graph_objs import Figure # type: ignore[import-untyped
|
32
|
-
from plotly.io import to_html # type: ignore[import-untyped
|
33
|
-
from plotly.offline import plot # type: ignore[import-untyped
|
34
|
-
from scipy.optimize import minimize # type: ignore[import-untyped
|
29
|
+
from plotly.graph_objs import Figure # type: ignore[import-untyped]
|
30
|
+
from plotly.io import to_html # type: ignore[import-untyped]
|
31
|
+
from plotly.offline import plot # type: ignore[import-untyped]
|
32
|
+
from scipy.optimize import minimize # type: ignore[import-untyped]
|
35
33
|
|
36
34
|
from .load_plotly import load_plotly_dict
|
37
35
|
from .owntypes import (
|
36
|
+
AtLeastOneFrameError,
|
38
37
|
LiteralLinePlotMode,
|
39
38
|
LiteralMinimizeMethods,
|
40
39
|
LiteralPlotlyJSlib,
|
41
40
|
LiteralPlotlyOutput,
|
41
|
+
MixedValuetypesError,
|
42
42
|
ValueType,
|
43
43
|
)
|
44
44
|
from .series import OpenTimeSeries
|
@@ -47,6 +47,10 @@ from .series import OpenTimeSeries
|
|
47
47
|
from .simulation import _random_generator
|
48
48
|
|
49
49
|
if TYPE_CHECKING: # pragma: no cover
|
50
|
+
# noinspection PyUnresolvedReferences
|
51
|
+
from collections.abc import Callable
|
52
|
+
|
53
|
+
from numpy.typing import NDArray
|
50
54
|
from pydantic import DirectoryPath
|
51
55
|
|
52
56
|
from .frame import OpenFrame
|
@@ -76,7 +80,7 @@ def simulate_portfolios(
|
|
76
80
|
seed: int
|
77
81
|
The seed for the random process
|
78
82
|
|
79
|
-
Returns
|
83
|
+
Returns:
|
80
84
|
-------
|
81
85
|
pandas.DataFrame
|
82
86
|
The resulting data
|
@@ -92,7 +96,7 @@ def simulate_portfolios(
|
|
92
96
|
log_ret = copi.tsdf.copy()
|
93
97
|
else:
|
94
98
|
msg = "Mix of series types will give inconsistent results"
|
95
|
-
raise
|
99
|
+
raise MixedValuetypesError(msg)
|
96
100
|
|
97
101
|
log_ret.columns = log_ret.columns.droplevel(level=1)
|
98
102
|
|
@@ -128,7 +132,7 @@ def simulate_portfolios(
|
|
128
132
|
|
129
133
|
|
130
134
|
# noinspection PyUnusedLocal
|
131
|
-
def efficient_frontier(
|
135
|
+
def efficient_frontier(
|
132
136
|
eframe: OpenFrame,
|
133
137
|
num_ports: int = 5000,
|
134
138
|
seed: int = 71,
|
@@ -157,7 +161,7 @@ def efficient_frontier( # noqa: C901
|
|
157
161
|
tweak: bool, default: True
|
158
162
|
cutting the frontier to exclude multiple points with almost the same risk
|
159
163
|
|
160
|
-
Returns
|
164
|
+
Returns:
|
161
165
|
-------
|
162
166
|
tuple[DataFrame, DataFrame, NDArray[float]]
|
163
167
|
The efficient frontier data, simulation data and optimal portfolio
|
@@ -176,7 +180,7 @@ def efficient_frontier( # noqa: C901
|
|
176
180
|
log_ret = copi.tsdf.copy()
|
177
181
|
else:
|
178
182
|
msg = "Mix of series types will give inconsistent results"
|
179
|
-
raise
|
183
|
+
raise MixedValuetypesError(msg)
|
180
184
|
|
181
185
|
log_ret.columns = log_ret.columns.droplevel(level=1)
|
182
186
|
|
@@ -189,8 +193,8 @@ def efficient_frontier( # noqa: C901
|
|
189
193
|
|
190
194
|
frontier_max = cleaned_arithmetic_means.max()
|
191
195
|
|
192
|
-
def _check_sum(weights: NDArray[float64]) ->
|
193
|
-
return npsum(weights) - 1
|
196
|
+
def _check_sum(weights: NDArray[float64]) -> float:
|
197
|
+
return cast("float", npsum(weights) - 1)
|
194
198
|
|
195
199
|
def _get_ret_vol_sr(
|
196
200
|
lg_ret: DataFrame,
|
@@ -200,7 +204,7 @@ def efficient_frontier( # noqa: C901
|
|
200
204
|
ret = npsum(lg_ret.mean() * weights) * per_in_yr
|
201
205
|
volatility = sqrt(weights.T @ (lg_ret.cov() * per_in_yr @ weights))
|
202
206
|
sr = ret / volatility
|
203
|
-
return cast(NDArray[float64], array([ret, volatility, sr]))
|
207
|
+
return cast("NDArray[float64]", array([ret, volatility, sr]))
|
204
208
|
|
205
209
|
def _diff_return(
|
206
210
|
lg_ret: DataFrame,
|
@@ -209,14 +213,14 @@ def efficient_frontier( # noqa: C901
|
|
209
213
|
poss_return: float,
|
210
214
|
) -> float64:
|
211
215
|
return cast(
|
212
|
-
float64,
|
216
|
+
"float64",
|
213
217
|
_get_ret_vol_sr(lg_ret=lg_ret, weights=weights, per_in_yr=per_in_yr)[0]
|
214
218
|
- poss_return,
|
215
219
|
)
|
216
220
|
|
217
221
|
def _neg_sharpe(weights: NDArray[float64]) -> float64:
|
218
222
|
return cast(
|
219
|
-
float64,
|
223
|
+
"float64",
|
220
224
|
_get_ret_vol_sr(
|
221
225
|
lg_ret=log_ret,
|
222
226
|
weights=weights,
|
@@ -229,7 +233,7 @@ def efficient_frontier( # noqa: C901
|
|
229
233
|
weights: NDArray[float64],
|
230
234
|
) -> float64:
|
231
235
|
return cast(
|
232
|
-
float64,
|
236
|
+
"float64",
|
233
237
|
_get_ret_vol_sr(
|
234
238
|
lg_ret=log_ret,
|
235
239
|
weights=weights,
|
@@ -262,7 +266,7 @@ def efficient_frontier( # noqa: C901
|
|
262
266
|
|
263
267
|
for possible_return in frontier_y:
|
264
268
|
cons = cast(
|
265
|
-
dict[str, str | Callable[[float, NDArray[float64]], float64]],
|
269
|
+
"dict[str, str | Callable[[float, NDArray[float64]], float64]]",
|
266
270
|
(
|
267
271
|
{"type": "eq", "fun": _check_sum},
|
268
272
|
{
|
@@ -344,7 +348,7 @@ def constrain_optimized_portfolios(
|
|
344
348
|
minimize_method: LiteralMinimizeMethods, default: SLSQP
|
345
349
|
The method passed into the scipy.minimize function
|
346
350
|
|
347
|
-
Returns
|
351
|
+
Returns:
|
348
352
|
-------
|
349
353
|
tuple[OpenFrame, OpenTimeSeries, OpenFrame, OpenTimeSeries]
|
350
354
|
The constrained optimal portfolio data
|
@@ -404,7 +408,7 @@ def prepare_plot_data(
|
|
404
408
|
optimized: DataFrame
|
405
409
|
Data optimized with the efficient_frontier method
|
406
410
|
|
407
|
-
Returns
|
411
|
+
Returns:
|
408
412
|
-------
|
409
413
|
DataFrame
|
410
414
|
The data prepared with mean returns, volatility and weights
|
@@ -414,7 +418,7 @@ def prepare_plot_data(
|
|
414
418
|
[
|
415
419
|
f"{wgt:.1%} {nm}"
|
416
420
|
for wgt, nm in zip(
|
417
|
-
cast(list[float], assets.weights),
|
421
|
+
cast("list[float]", assets.weights),
|
418
422
|
assets.columns_lvl_zero,
|
419
423
|
strict=True,
|
420
424
|
)
|
@@ -445,7 +449,7 @@ def prepare_plot_data(
|
|
445
449
|
return plotframe
|
446
450
|
|
447
451
|
|
448
|
-
def sharpeplot(
|
452
|
+
def sharpeplot(
|
449
453
|
sim_frame: DataFrame | None = None,
|
450
454
|
line_frame: DataFrame | None = None,
|
451
455
|
point_frame: DataFrame | None = None,
|
@@ -489,7 +493,7 @@ def sharpeplot( # noqa: C901
|
|
489
493
|
auto_open: bool, default: True
|
490
494
|
Determines whether to open a browser window with the plot
|
491
495
|
|
492
|
-
Returns
|
496
|
+
Returns:
|
493
497
|
-------
|
494
498
|
Figure
|
495
499
|
The scatter plot with simulated and optimized results
|
@@ -514,7 +518,7 @@ def sharpeplot( # noqa: C901
|
|
514
518
|
|
515
519
|
if sim_frame is None and line_frame is None and point_frame is None:
|
516
520
|
msg = "One of sim_frame, line_frame or point_frame must be provided."
|
517
|
-
raise
|
521
|
+
raise AtLeastOneFrameError(msg)
|
518
522
|
|
519
523
|
if sim_frame is not None:
|
520
524
|
returns.extend(list(sim_frame.loc[:, "ret"]))
|
@@ -552,12 +556,12 @@ def sharpeplot( # noqa: C901
|
|
552
556
|
|
553
557
|
if point_frame is not None:
|
554
558
|
colorway = cast(
|
555
|
-
dict[str, str | int | float | bool | list[str]],
|
559
|
+
"dict[str, str | int | float | bool | list[str]]",
|
556
560
|
fig["layout"],
|
557
561
|
).get("colorway")[: len(point_frame.columns)]
|
558
562
|
for col, clr in zip(point_frame.columns, colorway, strict=True):
|
559
|
-
returns.extend([point_frame.loc["ret", col]])
|
560
|
-
risk.extend([point_frame.loc["stdev", col]])
|
563
|
+
returns.extend([cast("float", point_frame.loc["ret", col])])
|
564
|
+
risk.extend([cast("float", point_frame.loc["stdev", col])])
|
561
565
|
figure.add_scatter(
|
562
566
|
x=[point_frame.loc["stdev", col]],
|
563
567
|
y=[point_frame.loc["ret", col]],
|
@@ -600,7 +604,7 @@ def sharpeplot( # noqa: C901
|
|
600
604
|
auto_open=auto_open,
|
601
605
|
auto_play=False,
|
602
606
|
link_text="",
|
603
|
-
include_plotlyjs=cast(bool, include_plotlyjs),
|
607
|
+
include_plotlyjs=cast("bool", include_plotlyjs),
|
604
608
|
config=fig["config"],
|
605
609
|
output_type=output_type,
|
606
610
|
)
|
@@ -611,7 +615,7 @@ def sharpeplot( # noqa: C901
|
|
611
615
|
fig=figure,
|
612
616
|
config=fig["config"],
|
613
617
|
auto_play=False,
|
614
|
-
include_plotlyjs=cast(bool, include_plotlyjs),
|
618
|
+
include_plotlyjs=cast("bool", include_plotlyjs),
|
615
619
|
full_html=False,
|
616
620
|
div_id=div_id,
|
617
621
|
)
|