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/frame.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,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
|
@@ -57,6 +58,7 @@ from .owntypes import (
|
|
57
58
|
NoWeightsError,
|
58
59
|
OpenFramePropertiesList,
|
59
60
|
RatioInputError,
|
61
|
+
ResampleDataLossError,
|
60
62
|
Self,
|
61
63
|
ValueType,
|
62
64
|
)
|
@@ -68,7 +70,7 @@ __all__ = ["OpenFrame"]
|
|
68
70
|
|
69
71
|
|
70
72
|
# noinspection PyUnresolvedReferences,PyTypeChecker
|
71
|
-
class OpenFrame(_CommonModel):
|
73
|
+
class OpenFrame(_CommonModel):
|
72
74
|
"""OpenFrame objects hold OpenTimeSeries in the list constituents.
|
73
75
|
|
74
76
|
The intended use is to allow comparisons across these timeseries.
|
@@ -93,7 +95,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
93
95
|
|
94
96
|
# noinspection PyMethodParameters
|
95
97
|
@field_validator("constituents") # type: ignore[misc]
|
96
|
-
def _check_labels_unique(
|
98
|
+
def _check_labels_unique(
|
97
99
|
cls: OpenFrame, # noqa: N805
|
98
100
|
tseries: list[OpenTimeSeries],
|
99
101
|
) -> list[OpenTimeSeries]:
|
@@ -128,7 +130,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
128
130
|
"""
|
129
131
|
copied_constituents = [ts.from_deepcopy() for ts in constituents]
|
130
132
|
|
131
|
-
super().__init__(
|
133
|
+
super().__init__(
|
132
134
|
constituents=copied_constituents,
|
133
135
|
weights=weights,
|
134
136
|
)
|
@@ -340,7 +342,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
340
342
|
The returns of the values in the series
|
341
343
|
|
342
344
|
"""
|
343
|
-
returns = self.tsdf.pct_change()
|
345
|
+
returns = self.tsdf.ffill().pct_change()
|
344
346
|
returns.iloc[0] = 0
|
345
347
|
new_labels: list[ValueType] = [ValueType.RTRN] * self.item_count
|
346
348
|
arrays: list[Index[Any], list[ValueType]] = [ # type: ignore[type-arg]
|
@@ -387,7 +389,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
387
389
|
"""
|
388
390
|
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
389
391
|
if not any(vtypes):
|
390
|
-
returns = self.tsdf.pct_change()
|
392
|
+
returns = self.tsdf.ffill().pct_change()
|
391
393
|
returns.iloc[0] = 0
|
392
394
|
elif all(vtypes):
|
393
395
|
returns = self.tsdf.copy()
|
@@ -424,12 +426,27 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
424
426
|
An OpenFrame object
|
425
427
|
|
426
428
|
"""
|
429
|
+
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
430
|
+
if not any(vtypes):
|
431
|
+
value_type = ValueType.PRICE
|
432
|
+
elif all(vtypes):
|
433
|
+
value_type = ValueType.RTRN
|
434
|
+
else:
|
435
|
+
msg = "Mix of series types will give inconsistent results"
|
436
|
+
raise MixedValuetypesError(msg)
|
437
|
+
|
427
438
|
self.tsdf.index = DatetimeIndex(self.tsdf.index)
|
428
|
-
|
439
|
+
if value_type == ValueType.PRICE:
|
440
|
+
self.tsdf = self.tsdf.resample(freq).last()
|
441
|
+
else:
|
442
|
+
self.tsdf = self.tsdf.resample(freq).sum()
|
429
443
|
self.tsdf.index = Index(d.date() for d in DatetimeIndex(self.tsdf.index))
|
430
444
|
for xerie in self.constituents:
|
431
445
|
xerie.tsdf.index = DatetimeIndex(xerie.tsdf.index)
|
432
|
-
|
446
|
+
if value_type == ValueType.PRICE:
|
447
|
+
xerie.tsdf = xerie.tsdf.resample(freq).last()
|
448
|
+
else:
|
449
|
+
xerie.tsdf = xerie.tsdf.resample(freq).sum()
|
433
450
|
xerie.tsdf.index = Index(
|
434
451
|
dejt.date() for dejt in DatetimeIndex(xerie.tsdf.index)
|
435
452
|
)
|
@@ -458,6 +475,15 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
458
475
|
An OpenFrame object
|
459
476
|
|
460
477
|
"""
|
478
|
+
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
479
|
+
if any(vtypes):
|
480
|
+
msg = (
|
481
|
+
"Do not run resample_to_business_period_ends on return series. "
|
482
|
+
"The operation will pick the last data point in the sparser series. "
|
483
|
+
"It will not sum returns and therefore data will be lost."
|
484
|
+
)
|
485
|
+
raise ResampleDataLossError(msg)
|
486
|
+
|
461
487
|
for xerie in self.constituents:
|
462
488
|
dates = _do_resample_to_business_period_ends(
|
463
489
|
data=xerie.tsdf,
|
@@ -530,7 +556,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
530
556
|
Series volatilities and correlation
|
531
557
|
|
532
558
|
"""
|
533
|
-
earlier, later = self.calc_range(
|
559
|
+
earlier, later = self.calc_range(
|
560
|
+
months_offset=months_from_last, from_dt=from_date, to_dt=to_date
|
561
|
+
)
|
534
562
|
if periods_in_a_year_fixed is None:
|
535
563
|
fraction = (later - earlier).days / 365.25
|
536
564
|
how_many = (
|
@@ -621,9 +649,13 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
621
649
|
Correlation matrix
|
622
650
|
|
623
651
|
"""
|
624
|
-
corr_matrix =
|
625
|
-
|
626
|
-
|
652
|
+
corr_matrix = (
|
653
|
+
self.tsdf.ffill()
|
654
|
+
.pct_change()
|
655
|
+
.corr(
|
656
|
+
method="pearson",
|
657
|
+
min_periods=1,
|
658
|
+
)
|
627
659
|
)
|
628
660
|
corr_matrix.columns = corr_matrix.columns.droplevel(level=1)
|
629
661
|
corr_matrix.index = corr_matrix.index.droplevel(level=1)
|
@@ -805,7 +837,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
805
837
|
Tracking Errors
|
806
838
|
|
807
839
|
"""
|
808
|
-
earlier, later = self.calc_range(
|
840
|
+
earlier, later = self.calc_range(
|
841
|
+
months_offset=months_from_last, from_dt=from_date, to_dt=to_date
|
842
|
+
)
|
809
843
|
fraction = (later - earlier).days / 365.25
|
810
844
|
|
811
845
|
msg = "base_column should be a tuple[str, ValueType] or an integer."
|
@@ -820,14 +854,15 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
820
854
|
self.tsdf.loc[:, base_column].name,
|
821
855
|
)[0]
|
822
856
|
elif isinstance(base_column, int):
|
823
|
-
shortdf =
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
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
|
+
)
|
831
866
|
short_label = cast("tuple[str, str]", self.tsdf.iloc[:, base_column].name)[
|
832
867
|
0
|
833
868
|
]
|
@@ -837,7 +872,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
837
872
|
if periods_in_a_year_fixed:
|
838
873
|
time_factor = float(periods_in_a_year_fixed)
|
839
874
|
else:
|
840
|
-
time_factor = float(shortdf.count() / fraction)
|
875
|
+
time_factor = float(cast("int64", shortdf.count()) / fraction)
|
841
876
|
|
842
877
|
terrors = []
|
843
878
|
for item in self.tsdf:
|
@@ -848,10 +883,8 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
848
883
|
:,
|
849
884
|
item,
|
850
885
|
]
|
851
|
-
relative =
|
852
|
-
vol = float(
|
853
|
-
relative.pct_change().std() * sqrt(time_factor),
|
854
|
-
)
|
886
|
+
relative = longdf.ffill().pct_change() - shortdf.ffill().pct_change()
|
887
|
+
vol = float(relative.std() * sqrt(time_factor))
|
855
888
|
terrors.append(vol)
|
856
889
|
|
857
890
|
return Series(
|
@@ -897,7 +930,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
897
930
|
Information Ratios
|
898
931
|
|
899
932
|
"""
|
900
|
-
earlier, later = self.calc_range(
|
933
|
+
earlier, later = self.calc_range(
|
934
|
+
months_offset=months_from_last, from_dt=from_date, to_dt=to_date
|
935
|
+
)
|
901
936
|
fraction = (later - earlier).days / 365.25
|
902
937
|
|
903
938
|
msg = "base_column should be a tuple[str, ValueType] or an integer."
|
@@ -912,14 +947,20 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
912
947
|
self.tsdf.loc[:, base_column].name,
|
913
948
|
)[0]
|
914
949
|
elif isinstance(base_column, int):
|
915
|
-
shortdf =
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
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
|
+
)
|
923
964
|
short_label = cast("tuple[str, str]", self.tsdf.iloc[:, base_column].name)[
|
924
965
|
0
|
925
966
|
]
|
@@ -929,7 +970,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
929
970
|
if periods_in_a_year_fixed:
|
930
971
|
time_factor = float(periods_in_a_year_fixed)
|
931
972
|
else:
|
932
|
-
time_factor = float(shortdf.count() / fraction)
|
973
|
+
time_factor = float(shortdf.count() / fraction) # type: ignore[arg-type]
|
933
974
|
|
934
975
|
ratios = []
|
935
976
|
for item in self.tsdf:
|
@@ -940,13 +981,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
940
981
|
:,
|
941
982
|
item,
|
942
983
|
]
|
943
|
-
relative =
|
944
|
-
ret = float(
|
945
|
-
|
946
|
-
)
|
947
|
-
vol = float(
|
948
|
-
relative.pct_change().std() * sqrt(time_factor),
|
949
|
-
)
|
984
|
+
relative = longdf.ffill().pct_change() - shortdf.ffill().pct_change()
|
985
|
+
ret = float(relative.mean() * time_factor)
|
986
|
+
vol = float(relative.std() * sqrt(time_factor))
|
950
987
|
ratios.append(ret / vol)
|
951
988
|
|
952
989
|
return Series(
|
@@ -1000,7 +1037,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1000
1037
|
|
1001
1038
|
"""
|
1002
1039
|
loss_limit: float = 0.0
|
1003
|
-
earlier, later = self.calc_range(
|
1040
|
+
earlier, later = self.calc_range(
|
1041
|
+
months_offset=months_from_last, from_dt=from_date, to_dt=to_date
|
1042
|
+
)
|
1004
1043
|
fraction = (later - earlier).days / 365.25
|
1005
1044
|
|
1006
1045
|
msg = "base_column should be a tuple[str, ValueType] or an integer."
|
@@ -1015,14 +1054,20 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1015
1054
|
self.tsdf.loc[:, base_column].name,
|
1016
1055
|
)[0]
|
1017
1056
|
elif isinstance(base_column, int):
|
1018
|
-
shortdf =
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
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
|
+
)
|
1026
1071
|
short_label = cast("tuple[str, str]", self.tsdf.iloc[:, base_column].name)[
|
1027
1072
|
0
|
1028
1073
|
]
|
@@ -1032,7 +1077,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1032
1077
|
if periods_in_a_year_fixed:
|
1033
1078
|
time_factor = float(periods_in_a_year_fixed)
|
1034
1079
|
else:
|
1035
|
-
time_factor = float(shortdf.count() / fraction)
|
1080
|
+
time_factor = float(shortdf.count() / fraction) # type: ignore[arg-type]
|
1036
1081
|
|
1037
1082
|
ratios = []
|
1038
1083
|
for item in self.tsdf:
|
@@ -1046,16 +1091,18 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1046
1091
|
msg = "ratio must be one of 'up', 'down' or 'both'."
|
1047
1092
|
if ratio == "up":
|
1048
1093
|
uparray = (
|
1049
|
-
longdf.
|
1050
|
-
|
1094
|
+
longdf.ffill()
|
1095
|
+
.pct_change()[
|
1096
|
+
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1051
1097
|
]
|
1052
1098
|
.add(1)
|
1053
1099
|
.to_numpy()
|
1054
1100
|
)
|
1055
1101
|
up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
|
1056
1102
|
upidxarray = (
|
1057
|
-
shortdf.
|
1058
|
-
|
1103
|
+
shortdf.ffill()
|
1104
|
+
.pct_change()[
|
1105
|
+
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1059
1106
|
]
|
1060
1107
|
.add(1)
|
1061
1108
|
.to_numpy()
|
@@ -1066,8 +1113,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1066
1113
|
ratios.append(up_rtrn / up_idx_return)
|
1067
1114
|
elif ratio == "down":
|
1068
1115
|
downarray = (
|
1069
|
-
longdf.
|
1070
|
-
|
1116
|
+
longdf.ffill()
|
1117
|
+
.pct_change()[
|
1118
|
+
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1071
1119
|
]
|
1072
1120
|
.add(1)
|
1073
1121
|
.to_numpy()
|
@@ -1076,8 +1124,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1076
1124
|
downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
|
1077
1125
|
)
|
1078
1126
|
downidxarray = (
|
1079
|
-
shortdf.
|
1080
|
-
|
1127
|
+
shortdf.ffill()
|
1128
|
+
.pct_change()[
|
1129
|
+
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1081
1130
|
]
|
1082
1131
|
.add(1)
|
1083
1132
|
.to_numpy()
|
@@ -1089,16 +1138,18 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1089
1138
|
ratios.append(down_return / down_idx_return)
|
1090
1139
|
elif ratio == "both":
|
1091
1140
|
uparray = (
|
1092
|
-
longdf.
|
1093
|
-
|
1141
|
+
longdf.ffill()
|
1142
|
+
.pct_change()[
|
1143
|
+
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1094
1144
|
]
|
1095
1145
|
.add(1)
|
1096
1146
|
.to_numpy()
|
1097
1147
|
)
|
1098
1148
|
up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
|
1099
1149
|
upidxarray = (
|
1100
|
-
shortdf.
|
1101
|
-
|
1150
|
+
shortdf.ffill()
|
1151
|
+
.pct_change()[
|
1152
|
+
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1102
1153
|
]
|
1103
1154
|
.add(1)
|
1104
1155
|
.to_numpy()
|
@@ -1107,8 +1158,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1107
1158
|
upidxarray.prod() ** (1 / (len(upidxarray) / time_factor)) - 1
|
1108
1159
|
)
|
1109
1160
|
downarray = (
|
1110
|
-
longdf.
|
1111
|
-
|
1161
|
+
longdf.ffill()
|
1162
|
+
.pct_change()[
|
1163
|
+
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1112
1164
|
]
|
1113
1165
|
.add(1)
|
1114
1166
|
.to_numpy()
|
@@ -1117,8 +1169,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1117
1169
|
downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
|
1118
1170
|
)
|
1119
1171
|
downidxarray = (
|
1120
|
-
shortdf.
|
1121
|
-
|
1172
|
+
shortdf.ffill()
|
1173
|
+
.pct_change()[
|
1174
|
+
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1122
1175
|
]
|
1123
1176
|
.add(1)
|
1124
1177
|
.to_numpy()
|
@@ -1173,15 +1226,13 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1173
1226
|
Beta as Co-variance of x & y divided by Variance of x
|
1174
1227
|
|
1175
1228
|
"""
|
1176
|
-
|
1177
|
-
|
1178
|
-
for x_value in self.tsdf.columns.get_level_values(1).to_numpy()
|
1179
|
-
):
|
1229
|
+
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
1230
|
+
if all(vtypes):
|
1180
1231
|
msg = "asset should be a tuple[str, ValueType] or an integer."
|
1181
1232
|
if isinstance(asset, tuple):
|
1182
1233
|
y_value = self.tsdf.loc[:, asset]
|
1183
1234
|
elif isinstance(asset, int):
|
1184
|
-
y_value = self.tsdf.iloc[:, asset]
|
1235
|
+
y_value = cast("DataFrame", self.tsdf.iloc[:, asset])
|
1185
1236
|
else:
|
1186
1237
|
raise TypeError(msg)
|
1187
1238
|
|
@@ -1189,36 +1240,35 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1189
1240
|
if isinstance(market, tuple):
|
1190
1241
|
x_value = self.tsdf.loc[:, market]
|
1191
1242
|
elif isinstance(market, int):
|
1192
|
-
x_value = self.tsdf.iloc[:, market]
|
1243
|
+
x_value = cast("DataFrame", self.tsdf.iloc[:, market])
|
1193
1244
|
else:
|
1194
1245
|
raise TypeError(msg)
|
1195
|
-
|
1246
|
+
elif not any(vtypes):
|
1196
1247
|
msg = "asset should be a tuple[str, ValueType] or an integer."
|
1197
1248
|
if isinstance(asset, tuple):
|
1198
|
-
y_value =
|
1199
|
-
self.tsdf.loc[:, asset] / self.tsdf.loc[:, asset].iloc[0],
|
1200
|
-
)
|
1249
|
+
y_value = self.tsdf.loc[:, asset].ffill().pct_change().iloc[1:]
|
1201
1250
|
elif isinstance(asset, int):
|
1202
|
-
y_value =
|
1203
|
-
self.tsdf.iloc[:, asset]
|
1251
|
+
y_value = cast(
|
1252
|
+
"DataFrame", self.tsdf.iloc[:, asset].ffill().pct_change().iloc[1:]
|
1204
1253
|
)
|
1205
1254
|
else:
|
1206
1255
|
raise TypeError(msg)
|
1207
|
-
|
1208
1256
|
msg = "market should be a tuple[str, ValueType] or an integer."
|
1257
|
+
|
1209
1258
|
if isinstance(market, tuple):
|
1210
|
-
x_value =
|
1211
|
-
self.tsdf.loc[:, market] / self.tsdf.loc[:, market].iloc[0],
|
1212
|
-
)
|
1259
|
+
x_value = self.tsdf.loc[:, market].ffill().pct_change().iloc[1:]
|
1213
1260
|
elif isinstance(market, int):
|
1214
|
-
x_value =
|
1215
|
-
|
1216
|
-
|
1261
|
+
x_value = cast(
|
1262
|
+
"DataFrame",
|
1263
|
+
self.tsdf.iloc[:, market].ffill().pct_change().iloc[1:],
|
1217
1264
|
)
|
1218
1265
|
else:
|
1219
1266
|
raise TypeError(msg)
|
1267
|
+
else:
|
1268
|
+
msg = "Mix of series types will give inconsistent results"
|
1269
|
+
raise MixedValuetypesError(msg)
|
1220
1270
|
|
1221
|
-
covariance = cov(y_value, x_value, ddof=dlta_degr_freedms)
|
1271
|
+
covariance = cov(m=y_value, y=x_value, ddof=dlta_degr_freedms)
|
1222
1272
|
beta = covariance[0, 1] / covariance[1, 1]
|
1223
1273
|
|
1224
1274
|
return float(beta)
|
@@ -1258,7 +1308,10 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1258
1308
|
self.tsdf.loc[:, y_column].name,
|
1259
1309
|
)[0]
|
1260
1310
|
elif isinstance(y_column, int):
|
1261
|
-
y_value =
|
1311
|
+
y_value = cast(
|
1312
|
+
"ndarray[tuple[int, int], dtype[Any]]",
|
1313
|
+
self.tsdf.iloc[:, y_column].to_numpy(),
|
1314
|
+
)
|
1262
1315
|
y_label = cast("tuple[str, str]", self.tsdf.iloc[:, y_column].name)[0]
|
1263
1316
|
else:
|
1264
1317
|
raise TypeError(msg)
|
@@ -1319,105 +1372,62 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1319
1372
|
Jensen's alpha
|
1320
1373
|
|
1321
1374
|
"""
|
1322
|
-
full_year = 1.0
|
1323
1375
|
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
1324
1376
|
if not any(vtypes):
|
1325
1377
|
msg = "asset should be a tuple[str, ValueType] or an integer."
|
1326
1378
|
if isinstance(asset, tuple):
|
1327
|
-
|
1328
|
-
|
1329
|
-
)
|
1330
|
-
if self.yearfrac > full_year:
|
1331
|
-
asset_cagr = (
|
1332
|
-
self.tsdf.loc[:, asset].iloc[-1]
|
1333
|
-
/ self.tsdf.loc[:, asset].iloc[0]
|
1334
|
-
) ** (1 / self.yearfrac) - 1
|
1335
|
-
else:
|
1336
|
-
asset_cagr = (
|
1337
|
-
self.tsdf.loc[:, asset].iloc[-1]
|
1338
|
-
/ self.tsdf.loc[:, asset].iloc[0]
|
1339
|
-
- 1
|
1340
|
-
)
|
1379
|
+
asset_rtn = self.tsdf.loc[:, asset].ffill().pct_change().iloc[1:]
|
1380
|
+
asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
|
1341
1381
|
elif isinstance(asset, int):
|
1342
|
-
|
1343
|
-
self.tsdf.iloc[:, asset]
|
1382
|
+
asset_rtn = cast(
|
1383
|
+
"DataFrame", self.tsdf.iloc[:, asset].ffill().pct_change().iloc[1:]
|
1344
1384
|
)
|
1345
|
-
|
1346
|
-
asset_cagr = (
|
1347
|
-
cast("float", self.tsdf.iloc[-1, asset])
|
1348
|
-
/ cast("float", self.tsdf.iloc[0, asset])
|
1349
|
-
) ** (1 / self.yearfrac) - 1
|
1350
|
-
else:
|
1351
|
-
asset_cagr = (
|
1352
|
-
cast("float", self.tsdf.iloc[-1, asset])
|
1353
|
-
/ cast("float", self.tsdf.iloc[0, asset])
|
1354
|
-
- 1
|
1355
|
-
)
|
1385
|
+
asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
|
1356
1386
|
else:
|
1357
1387
|
raise TypeError(msg)
|
1358
1388
|
|
1359
1389
|
msg = "market should be a tuple[str, ValueType] or an integer."
|
1360
1390
|
if isinstance(market, tuple):
|
1361
|
-
|
1362
|
-
|
1363
|
-
)
|
1364
|
-
if self.yearfrac > full_year:
|
1365
|
-
market_cagr = (
|
1366
|
-
self.tsdf.loc[:, market].iloc[-1]
|
1367
|
-
/ self.tsdf.loc[:, market].iloc[0]
|
1368
|
-
) ** (1 / self.yearfrac) - 1
|
1369
|
-
else:
|
1370
|
-
market_cagr = (
|
1371
|
-
self.tsdf.loc[:, market].iloc[-1]
|
1372
|
-
/ self.tsdf.loc[:, market].iloc[0]
|
1373
|
-
- 1
|
1374
|
-
)
|
1391
|
+
market_rtn = self.tsdf.loc[:, market].ffill().pct_change().iloc[1:]
|
1392
|
+
market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
|
1375
1393
|
elif isinstance(market, int):
|
1376
|
-
|
1377
|
-
|
1378
|
-
|
1394
|
+
market_rtn = cast(
|
1395
|
+
"DataFrame",
|
1396
|
+
self.tsdf.iloc[:, market].ffill().pct_change().iloc[1:],
|
1379
1397
|
)
|
1380
|
-
|
1381
|
-
market_cagr = (
|
1382
|
-
cast("float", self.tsdf.iloc[-1, market])
|
1383
|
-
/ cast("float", self.tsdf.iloc[0, market])
|
1384
|
-
) ** (1 / self.yearfrac) - 1
|
1385
|
-
else:
|
1386
|
-
market_cagr = (
|
1387
|
-
cast("float", self.tsdf.iloc[-1, market])
|
1388
|
-
/ cast("float", self.tsdf.iloc[0, market])
|
1389
|
-
- 1
|
1390
|
-
)
|
1398
|
+
market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
|
1391
1399
|
else:
|
1392
1400
|
raise TypeError(msg)
|
1393
1401
|
elif all(vtypes):
|
1394
1402
|
msg = "asset should be a tuple[str, ValueType] or an integer."
|
1395
1403
|
if isinstance(asset, tuple):
|
1396
|
-
|
1397
|
-
|
1404
|
+
asset_rtn = self.tsdf.loc[:, asset]
|
1405
|
+
asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
|
1398
1406
|
elif isinstance(asset, int):
|
1399
|
-
|
1400
|
-
|
1407
|
+
asset_rtn = cast("DataFrame", self.tsdf.iloc[:, asset])
|
1408
|
+
asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
|
1401
1409
|
else:
|
1402
1410
|
raise TypeError(msg)
|
1403
1411
|
|
1404
1412
|
msg = "market should be a tuple[str, ValueType] or an integer."
|
1405
1413
|
if isinstance(market, tuple):
|
1406
|
-
|
1407
|
-
|
1414
|
+
market_rtn = self.tsdf.loc[:, market]
|
1415
|
+
market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
|
1408
1416
|
elif isinstance(market, int):
|
1409
|
-
|
1410
|
-
|
1417
|
+
market_rtn = cast("DataFrame", self.tsdf.iloc[:, market])
|
1418
|
+
market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
|
1411
1419
|
else:
|
1412
1420
|
raise TypeError(msg)
|
1413
1421
|
else:
|
1414
1422
|
msg = "Mix of series types will give inconsistent results"
|
1415
1423
|
raise MixedValuetypesError(msg)
|
1416
1424
|
|
1417
|
-
covariance = cov(m=
|
1425
|
+
covariance = cov(m=asset_rtn, y=market_rtn, ddof=dlta_degr_freedms)
|
1418
1426
|
beta = covariance[0, 1] / covariance[1, 1]
|
1419
1427
|
|
1420
|
-
return float(
|
1428
|
+
return float(
|
1429
|
+
asset_rtn_mean - riskfree_rate - beta * (market_rtn_mean - riskfree_rate)
|
1430
|
+
)
|
1421
1431
|
|
1422
1432
|
def make_portfolio(
|
1423
1433
|
self: Self,
|
@@ -1448,7 +1458,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1448
1458
|
|
1449
1459
|
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
1450
1460
|
if not any(vtypes):
|
1451
|
-
returns = self.tsdf.pct_change()
|
1461
|
+
returns = self.tsdf.ffill().pct_change()
|
1452
1462
|
returns.iloc[0] = 0
|
1453
1463
|
elif all(vtypes):
|
1454
1464
|
returns = self.tsdf.copy()
|
@@ -1523,11 +1533,14 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1523
1533
|
)
|
1524
1534
|
|
1525
1535
|
retseries = (
|
1526
|
-
relative.
|
1536
|
+
relative.ffill()
|
1537
|
+
.pct_change()
|
1538
|
+
.rolling(observations, min_periods=observations)
|
1539
|
+
.sum()
|
1527
1540
|
)
|
1528
1541
|
retdf = retseries.dropna().to_frame()
|
1529
1542
|
|
1530
|
-
voldf = relative.pct_change().rolling(
|
1543
|
+
voldf = relative.ffill().pct_change().rolling(
|
1531
1544
|
observations,
|
1532
1545
|
min_periods=observations,
|
1533
1546
|
).std() * sqrt(time_factor)
|
@@ -1573,9 +1586,13 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1573
1586
|
asset_label = cast("tuple[str, str]", self.tsdf.iloc[:, asset_column].name)[0]
|
1574
1587
|
beta_label = f"{asset_label} / {market_label}"
|
1575
1588
|
|
1576
|
-
rolling =
|
1577
|
-
|
1578
|
-
|
1589
|
+
rolling = (
|
1590
|
+
self.tsdf.ffill()
|
1591
|
+
.pct_change()
|
1592
|
+
.rolling(
|
1593
|
+
observations,
|
1594
|
+
min_periods=observations,
|
1595
|
+
)
|
1579
1596
|
)
|
1580
1597
|
|
1581
1598
|
rcov = rolling.cov(ddof=dlta_degr_freedms)
|
@@ -1595,7 +1612,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1595
1612
|
rollbeta.index = rollbeta.index.droplevel(level=1)
|
1596
1613
|
rollbeta.columns = MultiIndex.from_arrays([[beta_label], ["Beta"]])
|
1597
1614
|
|
1598
|
-
return rollbeta
|
1615
|
+
return cast("DataFrame", rollbeta)
|
1599
1616
|
|
1600
1617
|
def rolling_corr(
|
1601
1618
|
self: Self,
|
@@ -1630,10 +1647,11 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1630
1647
|
)
|
1631
1648
|
first_series = (
|
1632
1649
|
self.tsdf.iloc[:, first_column]
|
1650
|
+
.ffill()
|
1633
1651
|
.pct_change()[1:]
|
1634
1652
|
.rolling(observations, min_periods=observations)
|
1635
1653
|
)
|
1636
|
-
second_series = self.tsdf.iloc[:, second_column].pct_change()[1:]
|
1654
|
+
second_series = self.tsdf.iloc[:, second_column].ffill().pct_change()[1:]
|
1637
1655
|
corrdf = first_series.corr(other=second_series).dropna().to_frame()
|
1638
1656
|
corrdf.columns = MultiIndex.from_arrays(
|
1639
1657
|
[
|