openseries 1.9.4__tar.gz → 1.9.5__tar.gz
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-1.9.4 → openseries-1.9.5}/PKG-INFO +1 -1
- {openseries-1.9.4 → openseries-1.9.5}/openseries/_common_model.py +7 -14
- {openseries-1.9.4 → openseries-1.9.5}/openseries/datefixer.py +4 -4
- {openseries-1.9.4 → openseries-1.9.5}/openseries/frame.py +69 -42
- {openseries-1.9.4 → openseries-1.9.5}/openseries/plotly_layouts.json +1 -1
- {openseries-1.9.4 → openseries-1.9.5}/openseries/portfoliotools.py +8 -9
- {openseries-1.9.4 → openseries-1.9.5}/openseries/report.py +60 -53
- {openseries-1.9.4 → openseries-1.9.5}/openseries/series.py +20 -20
- {openseries-1.9.4 → openseries-1.9.5}/openseries/simulation.py +2 -2
- {openseries-1.9.4 → openseries-1.9.5}/pyproject.toml +7 -9
- {openseries-1.9.4 → openseries-1.9.5}/LICENSE.md +0 -0
- {openseries-1.9.4 → openseries-1.9.5}/README.md +0 -0
- {openseries-1.9.4 → openseries-1.9.5}/openseries/__init__.py +0 -0
- {openseries-1.9.4 → openseries-1.9.5}/openseries/_risk.py +0 -0
- {openseries-1.9.4 → openseries-1.9.5}/openseries/load_plotly.py +0 -0
- {openseries-1.9.4 → openseries-1.9.5}/openseries/owntypes.py +0 -0
- {openseries-1.9.4 → openseries-1.9.5}/openseries/plotly_captor_logo.json +0 -0
@@ -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="no-any-return"
|
11
10
|
from __future__ import annotations
|
12
11
|
|
13
12
|
import datetime as dt
|
@@ -67,7 +66,7 @@ from plotly.graph_objs import Figure # type: ignore[import-untyped]
|
|
67
66
|
from plotly.io import to_html # type: ignore[import-untyped]
|
68
67
|
from plotly.offline import plot # type: ignore[import-untyped]
|
69
68
|
from pydantic import BaseModel, ConfigDict, DirectoryPath, ValidationError
|
70
|
-
from scipy.stats import (
|
69
|
+
from scipy.stats import (
|
71
70
|
kurtosis,
|
72
71
|
norm,
|
73
72
|
skew,
|
@@ -1658,17 +1657,16 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
1658
1657
|
deep=True
|
1659
1658
|
)
|
1660
1659
|
result = [
|
1661
|
-
cvar_df.loc[:, x] # type: ignore[call-overload
|
1660
|
+
cvar_df.loc[:, x] # type: ignore[call-overload]
|
1662
1661
|
.ffill()
|
1663
1662
|
.pct_change()
|
1664
1663
|
.sort_values()
|
1665
1664
|
.iloc[
|
1666
1665
|
: ceil(
|
1667
|
-
(
|
1668
|
-
|
1669
|
-
|
1670
|
-
|
1671
|
-
.count(),
|
1666
|
+
cast(
|
1667
|
+
"int",
|
1668
|
+
(1 - level) * cvar_df.loc[:, x].ffill().pct_change().count(),
|
1669
|
+
)
|
1672
1670
|
),
|
1673
1671
|
]
|
1674
1672
|
.mean()
|
@@ -1807,12 +1805,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
1807
1805
|
)
|
1808
1806
|
fraction = (later - earlier).days / 365.25
|
1809
1807
|
|
1810
|
-
any_below_zero = any(
|
1811
|
-
self.tsdf.loc[[earlier, later]] # type: ignore[index]
|
1812
|
-
.lt(0.0)
|
1813
|
-
.any()
|
1814
|
-
.to_numpy()
|
1815
|
-
)
|
1808
|
+
any_below_zero = any(self.tsdf.loc[[earlier, later]].lt(0.0).any().to_numpy())
|
1816
1809
|
if zero in self.tsdf.loc[earlier].to_numpy() or any_below_zero:
|
1817
1810
|
msg = (
|
1818
1811
|
"Geometric return cannot be calculated due to "
|
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|
12
12
|
import datetime as dt
|
13
13
|
from typing import TYPE_CHECKING, cast
|
14
14
|
|
15
|
-
import exchange_calendars as exchcal
|
15
|
+
import exchange_calendars as exchcal # type: ignore[import-untyped]
|
16
16
|
from dateutil.relativedelta import relativedelta
|
17
17
|
from holidays import (
|
18
18
|
country_holidays,
|
@@ -163,7 +163,7 @@ def holiday_calendar(
|
|
163
163
|
custom_list = (
|
164
164
|
[custom_holidays]
|
165
165
|
if isinstance(custom_holidays, str)
|
166
|
-
else list(custom_holidays)
|
166
|
+
else list(custom_holidays)
|
167
167
|
)
|
168
168
|
hols.extend([date_fix(fixerdate=ddate) for ddate in custom_list])
|
169
169
|
|
@@ -255,7 +255,7 @@ def date_offset_foll(
|
|
255
255
|
while not is_busday(dates=new_date, busdaycal=calendar):
|
256
256
|
new_date += day_delta
|
257
257
|
|
258
|
-
return new_date
|
258
|
+
return new_date
|
259
259
|
|
260
260
|
|
261
261
|
def get_previous_business_day_before_today(
|
@@ -505,7 +505,7 @@ def _do_resample_to_business_period_ends(
|
|
505
505
|
dates = DatetimeIndex(
|
506
506
|
[copydata.index[0]]
|
507
507
|
+ [
|
508
|
-
date_offset_foll(
|
508
|
+
date_offset_foll(
|
509
509
|
raw_date=dt.date(d.year, d.month, 1)
|
510
510
|
+ relativedelta(months=1)
|
511
511
|
- dt.timedelta(days=1),
|
@@ -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,no-any-return"
|
11
10
|
from __future__ import annotations
|
12
11
|
|
13
12
|
from copy import deepcopy
|
@@ -18,6 +17,8 @@ from typing import TYPE_CHECKING, Any, cast
|
|
18
17
|
if TYPE_CHECKING: # pragma: no cover
|
19
18
|
import datetime as dt
|
20
19
|
|
20
|
+
from numpy import dtype, int64, ndarray
|
21
|
+
|
21
22
|
from numpy import (
|
22
23
|
array,
|
23
24
|
cov,
|
@@ -38,7 +39,7 @@ from pandas import (
|
|
38
39
|
merge,
|
39
40
|
)
|
40
41
|
from pydantic import field_validator
|
41
|
-
from sklearn.linear_model import LinearRegression
|
42
|
+
from sklearn.linear_model import LinearRegression # type: ignore[import-untyped]
|
42
43
|
|
43
44
|
from ._common_model import _CommonModel
|
44
45
|
from .datefixer import _do_resample_to_business_period_ends
|
@@ -69,7 +70,7 @@ __all__ = ["OpenFrame"]
|
|
69
70
|
|
70
71
|
|
71
72
|
# noinspection PyUnresolvedReferences,PyTypeChecker
|
72
|
-
class OpenFrame(_CommonModel):
|
73
|
+
class OpenFrame(_CommonModel):
|
73
74
|
"""OpenFrame objects hold OpenTimeSeries in the list constituents.
|
74
75
|
|
75
76
|
The intended use is to allow comparisons across these timeseries.
|
@@ -94,7 +95,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
94
95
|
|
95
96
|
# noinspection PyMethodParameters
|
96
97
|
@field_validator("constituents") # type: ignore[misc]
|
97
|
-
def _check_labels_unique(
|
98
|
+
def _check_labels_unique(
|
98
99
|
cls: OpenFrame, # noqa: N805
|
99
100
|
tseries: list[OpenTimeSeries],
|
100
101
|
) -> list[OpenTimeSeries]:
|
@@ -129,7 +130,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
129
130
|
"""
|
130
131
|
copied_constituents = [ts.from_deepcopy() for ts in constituents]
|
131
132
|
|
132
|
-
super().__init__(
|
133
|
+
super().__init__(
|
133
134
|
constituents=copied_constituents,
|
134
135
|
weights=weights,
|
135
136
|
)
|
@@ -853,14 +854,15 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
853
854
|
self.tsdf.loc[:, base_column].name,
|
854
855
|
)[0]
|
855
856
|
elif isinstance(base_column, int):
|
856
|
-
shortdf =
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
857
|
+
shortdf = cast(
|
858
|
+
"DataFrame",
|
859
|
+
self.tsdf.loc[cast("int", earlier) : cast("int", later)].iloc[
|
860
|
+
:, base_column
|
861
|
+
],
|
862
|
+
)
|
863
|
+
short_item = cast(
|
864
|
+
"tuple[str, ValueType]", self.tsdf.iloc[:, base_column].name
|
865
|
+
)
|
864
866
|
short_label = cast("tuple[str, str]", self.tsdf.iloc[:, base_column].name)[
|
865
867
|
0
|
866
868
|
]
|
@@ -870,7 +872,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
870
872
|
if periods_in_a_year_fixed:
|
871
873
|
time_factor = float(periods_in_a_year_fixed)
|
872
874
|
else:
|
873
|
-
time_factor = float(shortdf.count() / fraction)
|
875
|
+
time_factor = float(cast("int64", shortdf.count()) / fraction)
|
874
876
|
|
875
877
|
terrors = []
|
876
878
|
for item in self.tsdf:
|
@@ -945,14 +947,20 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
945
947
|
self.tsdf.loc[:, base_column].name,
|
946
948
|
)[0]
|
947
949
|
elif isinstance(base_column, int):
|
948
|
-
shortdf =
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
950
|
+
shortdf = cast(
|
951
|
+
"DataFrame",
|
952
|
+
self.tsdf.loc[cast("int", earlier) : cast("int", later)].iloc[
|
953
|
+
:,
|
954
|
+
base_column,
|
955
|
+
],
|
956
|
+
)
|
957
|
+
short_item = cast(
|
958
|
+
"tuple[str, ValueType]",
|
959
|
+
self.tsdf.iloc[
|
960
|
+
:,
|
961
|
+
base_column,
|
962
|
+
].name,
|
963
|
+
)
|
956
964
|
short_label = cast("tuple[str, str]", self.tsdf.iloc[:, base_column].name)[
|
957
965
|
0
|
958
966
|
]
|
@@ -962,7 +970,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
962
970
|
if periods_in_a_year_fixed:
|
963
971
|
time_factor = float(periods_in_a_year_fixed)
|
964
972
|
else:
|
965
|
-
time_factor = float(shortdf.count() / fraction)
|
973
|
+
time_factor = float(shortdf.count() / fraction) # type: ignore[arg-type]
|
966
974
|
|
967
975
|
ratios = []
|
968
976
|
for item in self.tsdf:
|
@@ -1046,14 +1054,20 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1046
1054
|
self.tsdf.loc[:, base_column].name,
|
1047
1055
|
)[0]
|
1048
1056
|
elif isinstance(base_column, int):
|
1049
|
-
shortdf =
|
1050
|
-
|
1051
|
-
|
1052
|
-
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1057
|
+
shortdf = cast(
|
1058
|
+
"DataFrame",
|
1059
|
+
self.tsdf.loc[cast("int", earlier) : cast("int", later)].iloc[
|
1060
|
+
:,
|
1061
|
+
base_column,
|
1062
|
+
],
|
1063
|
+
)
|
1064
|
+
short_item = cast(
|
1065
|
+
"tuple[str, ValueType]",
|
1066
|
+
self.tsdf.iloc[
|
1067
|
+
:,
|
1068
|
+
base_column,
|
1069
|
+
].name,
|
1070
|
+
)
|
1057
1071
|
short_label = cast("tuple[str, str]", self.tsdf.iloc[:, base_column].name)[
|
1058
1072
|
0
|
1059
1073
|
]
|
@@ -1063,7 +1077,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1063
1077
|
if periods_in_a_year_fixed:
|
1064
1078
|
time_factor = float(periods_in_a_year_fixed)
|
1065
1079
|
else:
|
1066
|
-
time_factor = float(shortdf.count() / fraction)
|
1080
|
+
time_factor = float(shortdf.count() / fraction) # type: ignore[arg-type]
|
1067
1081
|
|
1068
1082
|
ratios = []
|
1069
1083
|
for item in self.tsdf:
|
@@ -1218,7 +1232,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1218
1232
|
if isinstance(asset, tuple):
|
1219
1233
|
y_value = self.tsdf.loc[:, asset]
|
1220
1234
|
elif isinstance(asset, int):
|
1221
|
-
y_value = self.tsdf.iloc[:, asset]
|
1235
|
+
y_value = cast("DataFrame", self.tsdf.iloc[:, asset])
|
1222
1236
|
else:
|
1223
1237
|
raise TypeError(msg)
|
1224
1238
|
|
@@ -1226,7 +1240,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1226
1240
|
if isinstance(market, tuple):
|
1227
1241
|
x_value = self.tsdf.loc[:, market]
|
1228
1242
|
elif isinstance(market, int):
|
1229
|
-
x_value = self.tsdf.iloc[:, market]
|
1243
|
+
x_value = cast("DataFrame", self.tsdf.iloc[:, market])
|
1230
1244
|
else:
|
1231
1245
|
raise TypeError(msg)
|
1232
1246
|
elif not any(vtypes):
|
@@ -1234,7 +1248,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1234
1248
|
if isinstance(asset, tuple):
|
1235
1249
|
y_value = self.tsdf.loc[:, asset].ffill().pct_change().iloc[1:]
|
1236
1250
|
elif isinstance(asset, int):
|
1237
|
-
y_value =
|
1251
|
+
y_value = cast(
|
1252
|
+
"DataFrame", self.tsdf.iloc[:, asset].ffill().pct_change().iloc[1:]
|
1253
|
+
)
|
1238
1254
|
else:
|
1239
1255
|
raise TypeError(msg)
|
1240
1256
|
msg = "market should be a tuple[str, ValueType] or an integer."
|
@@ -1242,7 +1258,10 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1242
1258
|
if isinstance(market, tuple):
|
1243
1259
|
x_value = self.tsdf.loc[:, market].ffill().pct_change().iloc[1:]
|
1244
1260
|
elif isinstance(market, int):
|
1245
|
-
x_value =
|
1261
|
+
x_value = cast(
|
1262
|
+
"DataFrame",
|
1263
|
+
self.tsdf.iloc[:, market].ffill().pct_change().iloc[1:],
|
1264
|
+
)
|
1246
1265
|
else:
|
1247
1266
|
raise TypeError(msg)
|
1248
1267
|
else:
|
@@ -1289,7 +1308,10 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1289
1308
|
self.tsdf.loc[:, y_column].name,
|
1290
1309
|
)[0]
|
1291
1310
|
elif isinstance(y_column, int):
|
1292
|
-
y_value =
|
1311
|
+
y_value = cast(
|
1312
|
+
"ndarray[tuple[int, int], dtype[Any]]",
|
1313
|
+
self.tsdf.iloc[:, y_column].to_numpy(),
|
1314
|
+
)
|
1293
1315
|
y_label = cast("tuple[str, str]", self.tsdf.iloc[:, y_column].name)[0]
|
1294
1316
|
else:
|
1295
1317
|
raise TypeError(msg)
|
@@ -1357,7 +1379,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1357
1379
|
asset_rtn = self.tsdf.loc[:, asset].ffill().pct_change().iloc[1:]
|
1358
1380
|
asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
|
1359
1381
|
elif isinstance(asset, int):
|
1360
|
-
asset_rtn =
|
1382
|
+
asset_rtn = cast(
|
1383
|
+
"DataFrame", self.tsdf.iloc[:, asset].ffill().pct_change().iloc[1:]
|
1384
|
+
)
|
1361
1385
|
asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
|
1362
1386
|
else:
|
1363
1387
|
raise TypeError(msg)
|
@@ -1367,7 +1391,10 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1367
1391
|
market_rtn = self.tsdf.loc[:, market].ffill().pct_change().iloc[1:]
|
1368
1392
|
market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
|
1369
1393
|
elif isinstance(market, int):
|
1370
|
-
market_rtn =
|
1394
|
+
market_rtn = cast(
|
1395
|
+
"DataFrame",
|
1396
|
+
self.tsdf.iloc[:, market].ffill().pct_change().iloc[1:],
|
1397
|
+
)
|
1371
1398
|
market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
|
1372
1399
|
else:
|
1373
1400
|
raise TypeError(msg)
|
@@ -1377,7 +1404,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1377
1404
|
asset_rtn = self.tsdf.loc[:, asset]
|
1378
1405
|
asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
|
1379
1406
|
elif isinstance(asset, int):
|
1380
|
-
asset_rtn = self.tsdf.iloc[:, asset]
|
1407
|
+
asset_rtn = cast("DataFrame", self.tsdf.iloc[:, asset])
|
1381
1408
|
asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
|
1382
1409
|
else:
|
1383
1410
|
raise TypeError(msg)
|
@@ -1387,7 +1414,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1387
1414
|
market_rtn = self.tsdf.loc[:, market]
|
1388
1415
|
market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
|
1389
1416
|
elif isinstance(market, int):
|
1390
|
-
market_rtn = self.tsdf.iloc[:, market]
|
1417
|
+
market_rtn = cast("DataFrame", self.tsdf.iloc[:, market])
|
1391
1418
|
market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
|
1392
1419
|
else:
|
1393
1420
|
raise TypeError(msg)
|
@@ -1585,7 +1612,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1585
1612
|
rollbeta.index = rollbeta.index.droplevel(level=1)
|
1586
1613
|
rollbeta.columns = MultiIndex.from_arrays([[beta_label], ["Beta"]])
|
1587
1614
|
|
1588
|
-
return rollbeta
|
1615
|
+
return cast("DataFrame", rollbeta)
|
1589
1616
|
|
1590
1617
|
def rolling_corr(
|
1591
1618
|
self: Self,
|
@@ -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
|
@@ -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,
|
@@ -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,14 +281,16 @@ 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, index=copied.tsdf.columns, columns=["Jensen's Alpha"]
|
286
|
+
).T
|
281
287
|
rpt_df = concat([rpt_df, ar])
|
282
|
-
ir =
|
288
|
+
ir = copied.info_ratio_func()
|
283
289
|
ir.name = "Information Ratio"
|
284
290
|
ir.iloc[-1] = None
|
285
|
-
|
286
|
-
rpt_df = concat([rpt_df,
|
287
|
-
te_frame =
|
291
|
+
ir_df = ir.to_frame().T
|
292
|
+
rpt_df = concat([rpt_df, ir_df])
|
293
|
+
te_frame = copied.from_deepcopy()
|
288
294
|
te_frame.resample("7D")
|
289
295
|
with catch_warnings():
|
290
296
|
simplefilter("ignore")
|
@@ -298,11 +304,11 @@ def report_html(
|
|
298
304
|
else:
|
299
305
|
te.iloc[-1] = None
|
300
306
|
te.name = "Tracking Error (weekly)"
|
301
|
-
|
302
|
-
rpt_df = concat([rpt_df,
|
307
|
+
te_df = te.to_frame().T
|
308
|
+
rpt_df = concat([rpt_df, te_df])
|
303
309
|
|
304
|
-
if
|
305
|
-
crm =
|
310
|
+
if copied.yearfrac > 1.0:
|
311
|
+
crm = copied.from_deepcopy()
|
306
312
|
crm.resample("ME")
|
307
313
|
cru_save = Series(
|
308
314
|
data=[""] * crm.item_count,
|
@@ -322,10 +328,10 @@ def report_html(
|
|
322
328
|
else:
|
323
329
|
cru.iloc[-1] = None
|
324
330
|
cru.name = "Capture Ratio (monthly)"
|
325
|
-
|
326
|
-
rpt_df = concat([rpt_df,
|
331
|
+
cru_df = cru.to_frame().T
|
332
|
+
rpt_df = concat([rpt_df, cru_df])
|
327
333
|
formats.append("{:.2f}")
|
328
|
-
beta_frame =
|
334
|
+
beta_frame = copied.from_deepcopy()
|
329
335
|
beta_frame.resample("7D").value_nan_handle("drop")
|
330
336
|
beta_frame.to_cumret()
|
331
337
|
betas: list[str | float] = [
|
@@ -335,51 +341,50 @@ def report_html(
|
|
335
341
|
)
|
336
342
|
for bname in beta_frame.columns_lvl_zero[:-1]
|
337
343
|
]
|
338
|
-
# noinspection PyTypeChecker
|
339
344
|
betas.append("")
|
340
345
|
br = DataFrame(
|
341
346
|
data=betas,
|
342
|
-
index=
|
347
|
+
index=copied.tsdf.columns,
|
343
348
|
columns=["Index Beta (weekly)"],
|
344
349
|
).T
|
345
350
|
rpt_df = concat([rpt_df, br])
|
346
351
|
|
347
352
|
for item, f in zip(rpt_df.index, formats, strict=False):
|
348
353
|
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),
|
354
|
+
lambda x, fmt=f: x if (isinstance(x, str) or x is None) else fmt.format(x),
|
350
355
|
)
|
351
356
|
|
352
|
-
rpt_df.index = labels_init
|
357
|
+
rpt_df.index = Index(labels_init)
|
353
358
|
|
354
|
-
this_year =
|
355
|
-
this_month =
|
356
|
-
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(
|
357
362
|
"{:.2%}".format
|
358
363
|
)
|
359
364
|
ytd.name = "Year-to-Date"
|
360
365
|
mtd = cast(
|
361
366
|
"Series[float]",
|
362
|
-
|
367
|
+
copied.value_ret_calendar_period(year=this_year, month=this_month),
|
363
368
|
).map(
|
364
369
|
"{:.2%}".format,
|
365
370
|
)
|
366
371
|
mtd.name = "Month-to-Date"
|
367
|
-
|
368
|
-
|
369
|
-
rpt_df = concat([rpt_df,
|
370
|
-
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])
|
371
376
|
rpt_df = rpt_df.reindex(labels_final)
|
372
377
|
|
373
|
-
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])
|
374
379
|
rpt_df = rpt_df.reset_index()
|
375
380
|
|
376
|
-
colmns = ["", *
|
381
|
+
colmns = ["", *copied.columns_lvl_zero]
|
377
382
|
columns = [f"<b>{x}</b>" for x in colmns]
|
378
383
|
aligning = ["left"] + ["center"] * (len(columns) - 1)
|
379
384
|
|
380
385
|
col_even_color = "lightgrey"
|
381
386
|
col_odd_color = "white"
|
382
|
-
color_lst = ["grey"] + [col_odd_color] * (
|
387
|
+
color_lst = ["grey"] + [col_odd_color] * (copied.item_count - 1) + [col_even_color]
|
383
388
|
|
384
389
|
tablevalues = rpt_df.transpose().to_numpy().tolist()
|
385
390
|
cleanedtablevalues = list(tablevalues)[:-1]
|
@@ -424,7 +429,9 @@ def report_html(
|
|
424
429
|
figure.add_layout_image(logo)
|
425
430
|
|
426
431
|
figure.update_layout(fig.get("layout"))
|
427
|
-
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
|
+
)
|
428
435
|
|
429
436
|
if vertical_legend:
|
430
437
|
legend = {
|
@@ -445,7 +452,7 @@ def report_html(
|
|
445
452
|
|
446
453
|
figure.update_layout(
|
447
454
|
legend=legend,
|
448
|
-
colorway=colorway[:
|
455
|
+
colorway=colorway[: copied.item_count],
|
449
456
|
)
|
450
457
|
figure.update_xaxes(gridcolor="#EEEEEE", automargin=True, tickangle=-45)
|
451
458
|
figure.update_yaxes(tickformat=".2%", gridcolor="#EEEEEE", automargin=True)
|
@@ -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,
|
@@ -59,6 +59,9 @@ from .owntypes import (
|
|
59
59
|
ValueType,
|
60
60
|
)
|
61
61
|
|
62
|
+
FieldValidator = cast("Callable[..., Callable[..., Any]]", field_validator)
|
63
|
+
ModelValidator = cast("Callable[..., Callable[..., Any]]", model_validator)
|
64
|
+
|
62
65
|
logger = getLogger(__name__)
|
63
66
|
|
64
67
|
__all__ = ["OpenTimeSeries", "timeseries_chain"]
|
@@ -67,7 +70,7 @@ TypeOpenTimeSeries = TypeVar("TypeOpenTimeSeries", bound="OpenTimeSeries")
|
|
67
70
|
|
68
71
|
|
69
72
|
# noinspection PyUnresolvedReferences,PyNestedDecorators
|
70
|
-
class OpenTimeSeries(_CommonModel):
|
73
|
+
class OpenTimeSeries(_CommonModel):
|
71
74
|
"""OpenTimeSeries objects are at the core of the openseries package.
|
72
75
|
|
73
76
|
The intended use is to allow analyses of financial timeseries.
|
@@ -123,21 +126,21 @@ class OpenTimeSeries(_CommonModel): # type: ignore[misc]
|
|
123
126
|
isin: str | None = None
|
124
127
|
label: str | None = None
|
125
128
|
|
126
|
-
@
|
129
|
+
@FieldValidator("domestic", mode="before")
|
127
130
|
@classmethod
|
128
131
|
def _validate_domestic(cls, value: CurrencyStringType) -> CurrencyStringType:
|
129
132
|
"""Pydantic validator to ensure domestic field is validated."""
|
130
133
|
_ = Currency(ccy=value)
|
131
134
|
return value
|
132
135
|
|
133
|
-
@
|
136
|
+
@FieldValidator("countries", mode="before")
|
134
137
|
@classmethod
|
135
138
|
def _validate_countries(cls, value: CountriesType) -> CountriesType:
|
136
139
|
"""Pydantic validator to ensure countries field is validated."""
|
137
140
|
_ = Countries(countryinput=value)
|
138
141
|
return value
|
139
142
|
|
140
|
-
@
|
143
|
+
@FieldValidator("markets", mode="before")
|
141
144
|
@classmethod
|
142
145
|
def _validate_markets(
|
143
146
|
cls, value: list[str] | str | None
|
@@ -156,7 +159,7 @@ class OpenTimeSeries(_CommonModel): # type: ignore[misc]
|
|
156
159
|
raise MarketsNotStringNorListStrError(item_msg)
|
157
160
|
raise MarketsNotStringNorListStrError(msg)
|
158
161
|
|
159
|
-
@
|
162
|
+
@ModelValidator(mode="after")
|
160
163
|
def _dates_and_values_validate(self: Self) -> Self:
|
161
164
|
"""Pydantic validator to ensure dates and values are validated."""
|
162
165
|
values_list_length = len(self.values)
|
@@ -370,19 +373,17 @@ class OpenTimeSeries(_CommonModel): # type: ignore[misc]
|
|
370
373
|
An OpenTimeSeries object
|
371
374
|
|
372
375
|
"""
|
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
|
-
)
|
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]])
|
384
385
|
arr: list[float] = list(cumprod(insert(1 + deltas * rate / 365, 0, 1.0)))
|
385
|
-
dates = [d.strftime("%Y-%m-%d") for d in
|
386
|
+
dates = [d.strftime("%Y-%m-%d") for d in d_range]
|
386
387
|
|
387
388
|
return cls(
|
388
389
|
timeseries_id="",
|
@@ -552,8 +553,7 @@ class OpenTimeSeries(_CommonModel): # type: ignore[misc]
|
|
552
553
|
arr = array(self.values) / divider
|
553
554
|
|
554
555
|
deltas = array([i.days for i in self.tsdf.index[1:] - self.tsdf.index[:-1]])
|
555
|
-
|
556
|
-
arr = cumprod( # type: ignore[assignment]
|
556
|
+
arr = cumprod(
|
557
557
|
a=insert(arr=1.0 + deltas * arr[:-1] / days_in_year, obj=0, values=1.0)
|
558
558
|
)
|
559
559
|
|
@@ -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
1
|
[project]
|
2
2
|
name = "openseries"
|
3
|
-
version = "1.9.
|
3
|
+
version = "1.9.5"
|
4
4
|
description = "Tools for analyzing financial timeseries."
|
5
5
|
authors = [
|
6
6
|
{ name = "Martin Karrin", email = "martin.karrin@captor.se" },
|
@@ -60,15 +60,15 @@ dependencies = [
|
|
60
60
|
"Release Notes" = "https://github.com/CaptorAB/openseries/releases"
|
61
61
|
|
62
62
|
[tool.poetry.group.dev.dependencies]
|
63
|
-
black = ">=24.4.2,<27.0.0"
|
64
63
|
mypy = "1.17.1"
|
65
64
|
pandas-stubs = ">=2.1.2,<3.0.0"
|
66
65
|
pre-commit = ">=3.7.1,<6.0.0"
|
67
66
|
pytest = ">=8.2.2,<9.0.0"
|
68
67
|
pytest-cov = ">=5.0.0,<7.0.0"
|
69
68
|
pytest-xdist = ">=3.3.1,<5.0.0"
|
70
|
-
ruff = "0.12.
|
69
|
+
ruff = "0.12.12"
|
71
70
|
types-openpyxl = ">=3.1.2,<5.0.0"
|
71
|
+
scipy-stubs = ">=1.14.1.0,<2.0.0"
|
72
72
|
types-python-dateutil = ">=2.8.2,<4.0.0"
|
73
73
|
types-requests = ">=2.20.0,<3.0.0"
|
74
74
|
|
@@ -77,6 +77,7 @@ requires = ["poetry-core>=2.1.3"]
|
|
77
77
|
build-backend = "poetry.core.masonry.api"
|
78
78
|
|
79
79
|
[tool.mypy]
|
80
|
+
python_version = "3.13"
|
80
81
|
mypy_path = ["src"]
|
81
82
|
exclude = ["venv/*"]
|
82
83
|
cache_dir = ".mypy_cache"
|
@@ -85,10 +86,10 @@ strict = true
|
|
85
86
|
pretty = true
|
86
87
|
cache_fine_grained = true
|
87
88
|
incremental = true
|
88
|
-
ignore_missing_imports =
|
89
|
+
ignore_missing_imports = false
|
89
90
|
warn_unreachable = true
|
90
91
|
warn_redundant_casts = true
|
91
|
-
warn_unused_ignores =
|
92
|
+
warn_unused_ignores = true
|
92
93
|
disallow_any_generics = true
|
93
94
|
check_untyped_defs = true
|
94
95
|
no_implicit_reexport = true
|
@@ -100,9 +101,6 @@ init_forbid_extra = true
|
|
100
101
|
init_typed = true
|
101
102
|
warn_required_dynamic_aliases = true
|
102
103
|
|
103
|
-
[tool.black]
|
104
|
-
line-length = 87
|
105
|
-
|
106
104
|
[tool.ruff]
|
107
105
|
target-version = "py310"
|
108
106
|
line-length = 87
|
@@ -113,7 +111,7 @@ ignore = ["COM812", "D203", "D213"]
|
|
113
111
|
fixable = ["ALL"]
|
114
112
|
mccabe = { max-complexity = 18 }
|
115
113
|
pydocstyle = { convention = "google" }
|
116
|
-
pylint = { max-args = 19, max-branches = 24, max-statements =
|
114
|
+
pylint = { max-args = 19, max-branches = 24, max-statements = 130 }
|
117
115
|
|
118
116
|
[tool.pytest.ini_options]
|
119
117
|
testpaths = ["tests"]
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|