openseries 1.9.1__py3-none-any.whl → 1.9.3__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 +98 -65
- openseries/frame.py +161 -115
- openseries/load_plotly.py +1 -1
- openseries/owntypes.py +3 -14
- openseries/portfoliotools.py +11 -7
- openseries/report.py +9 -5
- openseries/series.py +6 -6
- openseries/simulation.py +6 -10
- {openseries-1.9.1.dist-info → openseries-1.9.3.dist-info}/METADATA +67 -65
- openseries-1.9.3.dist-info/RECORD +17 -0
- openseries-1.9.1.dist-info/RECORD +0 -17
- {openseries-1.9.1.dist-info → openseries-1.9.3.dist-info}/LICENSE.md +0 -0
- {openseries-1.9.1.dist-info → openseries-1.9.3.dist-info}/WHEEL +0 -0
openseries/frame.py
CHANGED
@@ -7,22 +7,17 @@ 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="
|
10
|
+
# mypy: disable-error-code="assignment,no-any-return"
|
11
11
|
from __future__ import annotations
|
12
12
|
|
13
13
|
from copy import deepcopy
|
14
14
|
from functools import reduce
|
15
15
|
from logging import getLogger
|
16
|
-
from typing import TYPE_CHECKING, cast
|
16
|
+
from typing import TYPE_CHECKING, Any, cast
|
17
17
|
|
18
18
|
if TYPE_CHECKING: # pragma: no cover
|
19
19
|
import datetime as dt
|
20
20
|
|
21
|
-
from statsmodels.regression.linear_model import ( # type: ignore[import-untyped]
|
22
|
-
OLSResults,
|
23
|
-
)
|
24
|
-
|
25
|
-
import statsmodels.api as sm # type: ignore[import-untyped]
|
26
21
|
from numpy import (
|
27
22
|
array,
|
28
23
|
cov,
|
@@ -31,7 +26,6 @@ from numpy import (
|
|
31
26
|
log,
|
32
27
|
nan,
|
33
28
|
sqrt,
|
34
|
-
square,
|
35
29
|
std,
|
36
30
|
)
|
37
31
|
from pandas import (
|
@@ -44,6 +38,7 @@ from pandas import (
|
|
44
38
|
merge,
|
45
39
|
)
|
46
40
|
from pydantic import field_validator
|
41
|
+
from sklearn.linear_model import LinearRegression
|
47
42
|
|
48
43
|
from ._common_model import _CommonModel
|
49
44
|
from .datefixer import _do_resample_to_business_period_ends
|
@@ -54,8 +49,6 @@ from .owntypes import (
|
|
54
49
|
LiteralCaptureRatio,
|
55
50
|
LiteralFrameProps,
|
56
51
|
LiteralHowMerge,
|
57
|
-
LiteralOlsFitCovType,
|
58
|
-
LiteralOlsFitMethod,
|
59
52
|
LiteralPandasReindexMethod,
|
60
53
|
LiteralPortfolioWeightings,
|
61
54
|
LiteralTrunc,
|
@@ -75,7 +68,7 @@ __all__ = ["OpenFrame"]
|
|
75
68
|
|
76
69
|
|
77
70
|
# noinspection PyUnresolvedReferences,PyTypeChecker
|
78
|
-
class OpenFrame(_CommonModel):
|
71
|
+
class OpenFrame(_CommonModel): # type: ignore[misc]
|
79
72
|
"""OpenFrame objects hold OpenTimeSeries in the list constituents.
|
80
73
|
|
81
74
|
The intended use is to allow comparisons across these timeseries.
|
@@ -133,12 +126,14 @@ class OpenFrame(_CommonModel):
|
|
133
126
|
Object of the class OpenFrame
|
134
127
|
|
135
128
|
"""
|
129
|
+
copied_constituents = [ts.from_deepcopy() for ts in constituents]
|
130
|
+
|
136
131
|
super().__init__( # type: ignore[call-arg]
|
137
|
-
constituents=
|
132
|
+
constituents=copied_constituents,
|
138
133
|
weights=weights,
|
139
134
|
)
|
140
135
|
|
141
|
-
self.constituents =
|
136
|
+
self.constituents = copied_constituents
|
142
137
|
self.weights = weights
|
143
138
|
self._set_tsdf()
|
144
139
|
|
@@ -345,10 +340,13 @@ class OpenFrame(_CommonModel):
|
|
345
340
|
The returns of the values in the series
|
346
341
|
|
347
342
|
"""
|
348
|
-
returns = self.tsdf.
|
343
|
+
returns = self.tsdf.pct_change()
|
349
344
|
returns.iloc[0] = 0
|
350
|
-
new_labels = [ValueType.RTRN] * self.item_count
|
351
|
-
arrays = [
|
345
|
+
new_labels: list[ValueType] = [ValueType.RTRN] * self.item_count
|
346
|
+
arrays: list[Index[Any], list[ValueType]] = [ # type: ignore[type-arg]
|
347
|
+
self.tsdf.columns.get_level_values(0),
|
348
|
+
new_labels,
|
349
|
+
]
|
352
350
|
returns.columns = MultiIndex.from_arrays(arrays=arrays)
|
353
351
|
self.tsdf = returns.copy()
|
354
352
|
return self
|
@@ -370,8 +368,11 @@ class OpenFrame(_CommonModel):
|
|
370
368
|
"""
|
371
369
|
self.tsdf = self.tsdf.diff(periods=periods)
|
372
370
|
self.tsdf.iloc[0] = 0
|
373
|
-
new_labels = [ValueType.RTRN] * self.item_count
|
374
|
-
arrays = [
|
371
|
+
new_labels: list[ValueType] = [ValueType.RTRN] * self.item_count
|
372
|
+
arrays: list[Index[Any], list[ValueType]] = [ # type: ignore[type-arg]
|
373
|
+
self.tsdf.columns.get_level_values(0),
|
374
|
+
new_labels,
|
375
|
+
]
|
375
376
|
self.tsdf.columns = MultiIndex.from_arrays(arrays)
|
376
377
|
return self
|
377
378
|
|
@@ -386,7 +387,7 @@ class OpenFrame(_CommonModel):
|
|
386
387
|
"""
|
387
388
|
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
388
389
|
if not any(vtypes):
|
389
|
-
returns = self.tsdf.
|
390
|
+
returns = self.tsdf.pct_change()
|
390
391
|
returns.iloc[0] = 0
|
391
392
|
elif all(vtypes):
|
392
393
|
returns = self.tsdf.copy()
|
@@ -398,8 +399,11 @@ class OpenFrame(_CommonModel):
|
|
398
399
|
returns = returns.add(1.0)
|
399
400
|
self.tsdf = returns.cumprod(axis=0) / returns.iloc[0]
|
400
401
|
|
401
|
-
new_labels = [ValueType.PRICE] * self.item_count
|
402
|
-
arrays = [
|
402
|
+
new_labels: list[ValueType] = [ValueType.PRICE] * self.item_count
|
403
|
+
arrays: list[Index[Any], list[ValueType]] = [ # type: ignore[type-arg]
|
404
|
+
self.tsdf.columns.get_level_values(0),
|
405
|
+
new_labels,
|
406
|
+
]
|
403
407
|
self.tsdf.columns = MultiIndex.from_arrays(arrays)
|
404
408
|
return self
|
405
409
|
|
@@ -484,6 +488,7 @@ class OpenFrame(_CommonModel):
|
|
484
488
|
dlta_degr_freedms: int = 0,
|
485
489
|
first_column: int = 0,
|
486
490
|
second_column: int = 1,
|
491
|
+
corr_scale: float = 2.0,
|
487
492
|
months_from_last: int | None = None,
|
488
493
|
from_date: dt.date | None = None,
|
489
494
|
to_date: dt.date | None = None,
|
@@ -506,6 +511,8 @@ class OpenFrame(_CommonModel):
|
|
506
511
|
Column of first timeseries.
|
507
512
|
second_column: int, default: 1
|
508
513
|
Column of second timeseries.
|
514
|
+
corr_scale: float, default: 2.0
|
515
|
+
Correlation scale factor.
|
509
516
|
months_from_last : int, optional
|
510
517
|
number of months offset as positive integer. Overrides use of from_date
|
511
518
|
and to_date
|
@@ -548,9 +555,7 @@ class OpenFrame(_CommonModel):
|
|
548
555
|
data = self.tsdf.loc[cast("int", earlier) : cast("int", later)].copy()
|
549
556
|
|
550
557
|
for rtn in cols:
|
551
|
-
data[rtn, ValueType.RTRN] = (
|
552
|
-
data.loc[:, (rtn, ValueType.PRICE)].apply(log).diff()
|
553
|
-
)
|
558
|
+
data[rtn, ValueType.RTRN] = log(data.loc[:, (rtn, ValueType.PRICE)]).diff()
|
554
559
|
|
555
560
|
raw_one = [
|
556
561
|
data.loc[:, (cols[0], ValueType.RTRN)]
|
@@ -571,34 +576,39 @@ class OpenFrame(_CommonModel):
|
|
571
576
|
ddof=dlta_degr_freedms,
|
572
577
|
)[0][1],
|
573
578
|
]
|
574
|
-
raw_corr = [raw_cov[0] / (2 * raw_one[0] * raw_two[0])]
|
575
579
|
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
580
|
+
r1 = data.loc[:, (cols[0], ValueType.RTRN)]
|
581
|
+
r2 = data.loc[:, (cols[1], ValueType.RTRN)]
|
582
|
+
|
583
|
+
alpha = 1.0 - lmbda
|
584
|
+
|
585
|
+
s1 = (r1.pow(2) * time_factor).copy()
|
586
|
+
s2 = (r2.pow(2) * time_factor).copy()
|
587
|
+
sc = (r1 * r2 * time_factor).copy()
|
588
|
+
|
589
|
+
s1.iloc[0] = float(raw_one[0] ** 2)
|
590
|
+
s2.iloc[0] = float(raw_two[0] ** 2)
|
591
|
+
sc.iloc[0] = float(raw_cov[0])
|
592
|
+
|
593
|
+
m1 = s1.ewm(alpha=alpha, adjust=False).mean()
|
594
|
+
m2 = s2.ewm(alpha=alpha, adjust=False).mean()
|
595
|
+
mc = sc.ewm(alpha=alpha, adjust=False).mean()
|
596
|
+
|
597
|
+
m1v = m1.to_numpy(copy=False)
|
598
|
+
m2v = m2.to_numpy(copy=False)
|
599
|
+
mcv = mc.to_numpy(copy=False)
|
600
|
+
|
601
|
+
vol1 = sqrt(m1v)
|
602
|
+
vol2 = sqrt(m2v)
|
603
|
+
denom = corr_scale * vol1 * vol2
|
604
|
+
|
605
|
+
corr = mcv / denom
|
606
|
+
corr[denom == 0.0] = nan
|
597
607
|
|
598
608
|
return DataFrame(
|
599
609
|
index=[*cols, corr_label],
|
600
610
|
columns=data.index,
|
601
|
-
data=[
|
611
|
+
data=[vol1, vol2, corr],
|
602
612
|
).T
|
603
613
|
|
604
614
|
@property
|
@@ -611,13 +621,9 @@ class OpenFrame(_CommonModel):
|
|
611
621
|
Correlation matrix
|
612
622
|
|
613
623
|
"""
|
614
|
-
corr_matrix = (
|
615
|
-
|
616
|
-
|
617
|
-
.corr(
|
618
|
-
method="pearson",
|
619
|
-
min_periods=1,
|
620
|
-
)
|
624
|
+
corr_matrix = self.tsdf.pct_change().corr(
|
625
|
+
method="pearson",
|
626
|
+
min_periods=1,
|
621
627
|
)
|
622
628
|
corr_matrix.columns = corr_matrix.columns.droplevel(level=1)
|
623
629
|
corr_matrix.index = corr_matrix.index.droplevel(level=1)
|
@@ -844,7 +850,7 @@ class OpenFrame(_CommonModel):
|
|
844
850
|
]
|
845
851
|
relative = 1.0 + longdf - shortdf
|
846
852
|
vol = float(
|
847
|
-
relative.
|
853
|
+
relative.pct_change().std() * sqrt(time_factor),
|
848
854
|
)
|
849
855
|
terrors.append(vol)
|
850
856
|
|
@@ -936,10 +942,10 @@ class OpenFrame(_CommonModel):
|
|
936
942
|
]
|
937
943
|
relative = 1.0 + longdf - shortdf
|
938
944
|
ret = float(
|
939
|
-
relative.
|
945
|
+
relative.pct_change().mean() * time_factor,
|
940
946
|
)
|
941
947
|
vol = float(
|
942
|
-
relative.
|
948
|
+
relative.pct_change().std() * sqrt(time_factor),
|
943
949
|
)
|
944
950
|
ratios.append(ret / vol)
|
945
951
|
|
@@ -1040,18 +1046,16 @@ class OpenFrame(_CommonModel):
|
|
1040
1046
|
msg = "ratio must be one of 'up', 'down' or 'both'."
|
1041
1047
|
if ratio == "up":
|
1042
1048
|
uparray = (
|
1043
|
-
longdf.
|
1044
|
-
|
1045
|
-
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1049
|
+
longdf.pct_change()[
|
1050
|
+
shortdf.pct_change().to_numpy() > loss_limit
|
1046
1051
|
]
|
1047
1052
|
.add(1)
|
1048
1053
|
.to_numpy()
|
1049
1054
|
)
|
1050
1055
|
up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
|
1051
1056
|
upidxarray = (
|
1052
|
-
shortdf.
|
1053
|
-
|
1054
|
-
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1057
|
+
shortdf.pct_change()[
|
1058
|
+
shortdf.pct_change().to_numpy() > loss_limit
|
1055
1059
|
]
|
1056
1060
|
.add(1)
|
1057
1061
|
.to_numpy()
|
@@ -1062,9 +1066,8 @@ class OpenFrame(_CommonModel):
|
|
1062
1066
|
ratios.append(up_rtrn / up_idx_return)
|
1063
1067
|
elif ratio == "down":
|
1064
1068
|
downarray = (
|
1065
|
-
longdf.
|
1066
|
-
|
1067
|
-
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1069
|
+
longdf.pct_change()[
|
1070
|
+
shortdf.pct_change().to_numpy() < loss_limit
|
1068
1071
|
]
|
1069
1072
|
.add(1)
|
1070
1073
|
.to_numpy()
|
@@ -1073,9 +1076,8 @@ class OpenFrame(_CommonModel):
|
|
1073
1076
|
downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
|
1074
1077
|
)
|
1075
1078
|
downidxarray = (
|
1076
|
-
shortdf.
|
1077
|
-
|
1078
|
-
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1079
|
+
shortdf.pct_change()[
|
1080
|
+
shortdf.pct_change().to_numpy() < loss_limit
|
1079
1081
|
]
|
1080
1082
|
.add(1)
|
1081
1083
|
.to_numpy()
|
@@ -1087,18 +1089,16 @@ class OpenFrame(_CommonModel):
|
|
1087
1089
|
ratios.append(down_return / down_idx_return)
|
1088
1090
|
elif ratio == "both":
|
1089
1091
|
uparray = (
|
1090
|
-
longdf.
|
1091
|
-
|
1092
|
-
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1092
|
+
longdf.pct_change()[
|
1093
|
+
shortdf.pct_change().to_numpy() > loss_limit
|
1093
1094
|
]
|
1094
1095
|
.add(1)
|
1095
1096
|
.to_numpy()
|
1096
1097
|
)
|
1097
1098
|
up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
|
1098
1099
|
upidxarray = (
|
1099
|
-
shortdf.
|
1100
|
-
|
1101
|
-
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1100
|
+
shortdf.pct_change()[
|
1101
|
+
shortdf.pct_change().to_numpy() > loss_limit
|
1102
1102
|
]
|
1103
1103
|
.add(1)
|
1104
1104
|
.to_numpy()
|
@@ -1107,9 +1107,8 @@ class OpenFrame(_CommonModel):
|
|
1107
1107
|
upidxarray.prod() ** (1 / (len(upidxarray) / time_factor)) - 1
|
1108
1108
|
)
|
1109
1109
|
downarray = (
|
1110
|
-
longdf.
|
1111
|
-
|
1112
|
-
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1110
|
+
longdf.pct_change()[
|
1111
|
+
shortdf.pct_change().to_numpy() < loss_limit
|
1113
1112
|
]
|
1114
1113
|
.add(1)
|
1115
1114
|
.to_numpy()
|
@@ -1118,9 +1117,8 @@ class OpenFrame(_CommonModel):
|
|
1118
1117
|
downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
|
1119
1118
|
)
|
1120
1119
|
downidxarray = (
|
1121
|
-
shortdf.
|
1122
|
-
|
1123
|
-
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1120
|
+
shortdf.pct_change()[
|
1121
|
+
shortdf.pct_change().to_numpy() < loss_limit
|
1124
1122
|
]
|
1125
1123
|
.add(1)
|
1126
1124
|
.to_numpy()
|
@@ -1229,16 +1227,13 @@ class OpenFrame(_CommonModel):
|
|
1229
1227
|
self: Self,
|
1230
1228
|
y_column: tuple[str, ValueType] | int,
|
1231
1229
|
x_column: tuple[str, ValueType] | int,
|
1232
|
-
method: LiteralOlsFitMethod = "pinv",
|
1233
|
-
cov_type: LiteralOlsFitCovType = "nonrobust",
|
1234
1230
|
*,
|
1235
1231
|
fitted_series: bool = True,
|
1236
|
-
) ->
|
1232
|
+
) -> dict[str, float]:
|
1237
1233
|
"""Ordinary Least Squares fit.
|
1238
1234
|
|
1239
1235
|
Performs a linear regression and adds a new column with a fitted line
|
1240
1236
|
using Ordinary Least Squares fit
|
1241
|
-
https://www.statsmodels.org/stable/examples/notebooks/generated/ols.html.
|
1242
1237
|
|
1243
1238
|
Parameters
|
1244
1239
|
----------
|
@@ -1246,50 +1241,50 @@ class OpenFrame(_CommonModel):
|
|
1246
1241
|
The column level values of the dependent variable y
|
1247
1242
|
x_column: tuple[str, ValueType] | int
|
1248
1243
|
The column level values of the exogenous variable x
|
1249
|
-
method: LiteralOlsFitMethod, default: pinv
|
1250
|
-
Method to solve least squares problem
|
1251
|
-
cov_type: LiteralOlsFitCovType, default: nonrobust
|
1252
|
-
Covariance estimator
|
1253
1244
|
fitted_series: bool, default: True
|
1254
1245
|
If True the fit is added as a new column in the .tsdf Pandas.DataFrame
|
1255
1246
|
|
1256
1247
|
Returns:
|
1257
1248
|
-------
|
1258
|
-
|
1259
|
-
|
1249
|
+
dict[str, float]
|
1250
|
+
A dictionary with the coefficient, intercept and rsquared outputs.
|
1260
1251
|
|
1261
1252
|
"""
|
1262
1253
|
msg = "y_column should be a tuple[str, ValueType] or an integer."
|
1263
1254
|
if isinstance(y_column, tuple):
|
1264
|
-
y_value = self.tsdf.loc[:, y_column]
|
1255
|
+
y_value = self.tsdf.loc[:, y_column].to_numpy()
|
1265
1256
|
y_label = cast(
|
1266
1257
|
"tuple[str, str]",
|
1267
1258
|
self.tsdf.loc[:, y_column].name,
|
1268
1259
|
)[0]
|
1269
1260
|
elif isinstance(y_column, int):
|
1270
|
-
y_value = self.tsdf.iloc[:, y_column]
|
1261
|
+
y_value = self.tsdf.iloc[:, y_column].to_numpy()
|
1271
1262
|
y_label = cast("tuple[str, str]", self.tsdf.iloc[:, y_column].name)[0]
|
1272
1263
|
else:
|
1273
1264
|
raise TypeError(msg)
|
1274
1265
|
|
1275
1266
|
msg = "x_column should be a tuple[str, ValueType] or an integer."
|
1276
1267
|
if isinstance(x_column, tuple):
|
1277
|
-
x_value = self.tsdf.loc[:, x_column]
|
1268
|
+
x_value = self.tsdf.loc[:, x_column].to_numpy().reshape(-1, 1)
|
1278
1269
|
x_label = cast(
|
1279
1270
|
"tuple[str, str]",
|
1280
1271
|
self.tsdf.loc[:, x_column].name,
|
1281
1272
|
)[0]
|
1282
1273
|
elif isinstance(x_column, int):
|
1283
|
-
x_value = self.tsdf.iloc[:, x_column]
|
1274
|
+
x_value = self.tsdf.iloc[:, x_column].to_numpy().reshape(-1, 1)
|
1284
1275
|
x_label = cast("tuple[str, str]", self.tsdf.iloc[:, x_column].name)[0]
|
1285
1276
|
else:
|
1286
1277
|
raise TypeError(msg)
|
1287
1278
|
|
1288
|
-
|
1279
|
+
model = LinearRegression(fit_intercept=True)
|
1280
|
+
model.fit(x_value, y_value)
|
1289
1281
|
if fitted_series:
|
1290
|
-
self.tsdf[y_label, x_label] =
|
1291
|
-
|
1292
|
-
|
1282
|
+
self.tsdf[y_label, x_label] = model.predict(x_value)
|
1283
|
+
return {
|
1284
|
+
"coefficient": model.coef_[0],
|
1285
|
+
"intercept": model.intercept_,
|
1286
|
+
"rsquared": model.score(x_value, y_value),
|
1287
|
+
}
|
1293
1288
|
|
1294
1289
|
def jensen_alpha(
|
1295
1290
|
self: Self,
|
@@ -1419,7 +1414,7 @@ class OpenFrame(_CommonModel):
|
|
1419
1414
|
msg = "Mix of series types will give inconsistent results"
|
1420
1415
|
raise MixedValuetypesError(msg)
|
1421
1416
|
|
1422
|
-
covariance = cov(asset_log, market_log, ddof=dlta_degr_freedms)
|
1417
|
+
covariance = cov(m=asset_log, y=market_log, ddof=dlta_degr_freedms)
|
1423
1418
|
beta = covariance[0, 1] / covariance[1, 1]
|
1424
1419
|
|
1425
1420
|
return float(asset_cagr - riskfree_rate - beta * (market_cagr - riskfree_rate))
|
@@ -1453,7 +1448,7 @@ class OpenFrame(_CommonModel):
|
|
1453
1448
|
|
1454
1449
|
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
1455
1450
|
if not any(vtypes):
|
1456
|
-
returns = self.tsdf.
|
1451
|
+
returns = self.tsdf.pct_change()
|
1457
1452
|
returns.iloc[0] = 0
|
1458
1453
|
elif all(vtypes):
|
1459
1454
|
returns = self.tsdf.copy()
|
@@ -1528,14 +1523,11 @@ class OpenFrame(_CommonModel):
|
|
1528
1523
|
)
|
1529
1524
|
|
1530
1525
|
retseries = (
|
1531
|
-
relative.
|
1532
|
-
.pct_change()
|
1533
|
-
.rolling(observations, min_periods=observations)
|
1534
|
-
.sum()
|
1526
|
+
relative.pct_change().rolling(observations, min_periods=observations).sum()
|
1535
1527
|
)
|
1536
1528
|
retdf = retseries.dropna().to_frame()
|
1537
1529
|
|
1538
|
-
voldf = relative.
|
1530
|
+
voldf = relative.pct_change().rolling(
|
1539
1531
|
observations,
|
1540
1532
|
min_periods=observations,
|
1541
1533
|
).std() * sqrt(time_factor)
|
@@ -1581,13 +1573,9 @@ class OpenFrame(_CommonModel):
|
|
1581
1573
|
asset_label = cast("tuple[str, str]", self.tsdf.iloc[:, asset_column].name)[0]
|
1582
1574
|
beta_label = f"{asset_label} / {market_label}"
|
1583
1575
|
|
1584
|
-
rolling = (
|
1585
|
-
|
1586
|
-
|
1587
|
-
.rolling(
|
1588
|
-
observations,
|
1589
|
-
min_periods=observations,
|
1590
|
-
)
|
1576
|
+
rolling = self.tsdf.pct_change().rolling(
|
1577
|
+
observations,
|
1578
|
+
min_periods=observations,
|
1591
1579
|
)
|
1592
1580
|
|
1593
1581
|
rcov = rolling.cov(ddof=dlta_degr_freedms)
|
@@ -1642,11 +1630,10 @@ class OpenFrame(_CommonModel):
|
|
1642
1630
|
)
|
1643
1631
|
first_series = (
|
1644
1632
|
self.tsdf.iloc[:, first_column]
|
1645
|
-
.ffill()
|
1646
1633
|
.pct_change()[1:]
|
1647
1634
|
.rolling(observations, min_periods=observations)
|
1648
1635
|
)
|
1649
|
-
second_series = self.tsdf.iloc[:, second_column].
|
1636
|
+
second_series = self.tsdf.iloc[:, second_column].pct_change()[1:]
|
1650
1637
|
corrdf = first_series.corr(other=second_series).dropna().to_frame()
|
1651
1638
|
corrdf.columns = MultiIndex.from_arrays(
|
1652
1639
|
[
|
@@ -1656,3 +1643,62 @@ class OpenFrame(_CommonModel):
|
|
1656
1643
|
)
|
1657
1644
|
|
1658
1645
|
return DataFrame(corrdf)
|
1646
|
+
|
1647
|
+
def multi_factor_linear_regression(
|
1648
|
+
self: Self,
|
1649
|
+
dependent_column: tuple[str, ValueType],
|
1650
|
+
) -> tuple[DataFrame, OpenTimeSeries]:
|
1651
|
+
"""Perform a multi-factor linear regression.
|
1652
|
+
|
1653
|
+
This function treats one specified column in the DataFrame as the dependent
|
1654
|
+
variable (y) and uses all remaining columns as independent variables (X).
|
1655
|
+
It utilizes a scikit-learn LinearRegression model and returns a DataFrame
|
1656
|
+
with summary output and an OpenTimeSeries of predicted values.
|
1657
|
+
|
1658
|
+
Parameters
|
1659
|
+
----------
|
1660
|
+
dependent_column: tuple[str, ValueType]
|
1661
|
+
A tuple key to select the column in the OpenFrame.tsdf.columns
|
1662
|
+
to use as the dependent variable
|
1663
|
+
|
1664
|
+
Returns:
|
1665
|
+
-------
|
1666
|
+
tuple[pandas.DataFrame, OpenTimeSeries]
|
1667
|
+
- A DataFrame with the R-squared, the intercept
|
1668
|
+
and the regression coefficients
|
1669
|
+
- An OpenTimeSeries of predicted values
|
1670
|
+
|
1671
|
+
Raises:
|
1672
|
+
KeyError: If the column tuple is not found in the OpenFrame.tsdf.columns
|
1673
|
+
ValueError: If not all series are returnseries (ValueType.RTRN)
|
1674
|
+
"""
|
1675
|
+
key_msg = (
|
1676
|
+
f"Tuple ({dependent_column[0]}, "
|
1677
|
+
f"{dependent_column[1].value}) not found in data."
|
1678
|
+
)
|
1679
|
+
if dependent_column not in self.tsdf.columns:
|
1680
|
+
raise KeyError(key_msg)
|
1681
|
+
|
1682
|
+
vtype_msg = "All series should be of ValueType.RTRN."
|
1683
|
+
if not all(x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)):
|
1684
|
+
raise MixedValuetypesError(vtype_msg)
|
1685
|
+
|
1686
|
+
dependent = self.tsdf[dependent_column]
|
1687
|
+
factors = self.tsdf.drop(columns=[dependent_column])
|
1688
|
+
indx = ["R-square", "Intercept", *factors.columns.droplevel(level=1)]
|
1689
|
+
|
1690
|
+
model = LinearRegression()
|
1691
|
+
model.fit(factors, dependent)
|
1692
|
+
|
1693
|
+
predictions = OpenTimeSeries.from_arrays(
|
1694
|
+
name=f"Predicted {dependent_column[0]}",
|
1695
|
+
dates=[date.strftime("%Y-%m-%d") for date in self.tsdf.index],
|
1696
|
+
values=list(model.predict(factors)),
|
1697
|
+
valuetype=ValueType.RTRN,
|
1698
|
+
)
|
1699
|
+
|
1700
|
+
output = [model.score(factors, dependent), model.intercept_, *model.coef_]
|
1701
|
+
|
1702
|
+
result = DataFrame(data=output, index=indx, columns=[dependent_column[0]])
|
1703
|
+
|
1704
|
+
return result, predictions.to_cumret()
|
openseries/load_plotly.py
CHANGED
@@ -76,7 +76,7 @@ def load_plotly_dict(
|
|
76
76
|
with logofile.open(mode="r", encoding="utf-8") as logo_file:
|
77
77
|
logo = load(logo_file)
|
78
78
|
|
79
|
-
if _check_remote_file_existence(url=logo["source"])
|
79
|
+
if not _check_remote_file_existence(url=logo["source"]):
|
80
80
|
msg = f"Failed to add logo image from URL {logo['source']}"
|
81
81
|
logger.warning(msg)
|
82
82
|
logo = {}
|
openseries/owntypes.py
CHANGED
@@ -141,21 +141,7 @@ LiteralPlotlyHistogramHistNorm = Literal[
|
|
141
141
|
"density",
|
142
142
|
"probability density",
|
143
143
|
]
|
144
|
-
LiteralOlsFitMethod = Literal["pinv", "qr"]
|
145
144
|
LiteralPortfolioWeightings = Literal["eq_weights", "inv_vol"]
|
146
|
-
LiteralOlsFitCovType = Literal[
|
147
|
-
"nonrobust",
|
148
|
-
"fixed scale",
|
149
|
-
"HC0",
|
150
|
-
"HC1",
|
151
|
-
"HC2",
|
152
|
-
"HC3",
|
153
|
-
"HAC",
|
154
|
-
"hac-panel",
|
155
|
-
"hac-groupsum",
|
156
|
-
"cluster",
|
157
|
-
]
|
158
|
-
|
159
145
|
LiteralMinimizeMethods = Literal[
|
160
146
|
"SLSQP",
|
161
147
|
"Nelder-Mead",
|
@@ -181,6 +167,7 @@ LiteralSeriesProps = Literal[
|
|
181
167
|
"downside_deviation",
|
182
168
|
"ret_vol_ratio",
|
183
169
|
"sortino_ratio",
|
170
|
+
"kappa3_ratio",
|
184
171
|
"z_score",
|
185
172
|
"skew",
|
186
173
|
"kurtosis",
|
@@ -208,6 +195,7 @@ LiteralFrameProps = Literal[
|
|
208
195
|
"downside_deviation",
|
209
196
|
"ret_vol_ratio",
|
210
197
|
"sortino_ratio",
|
198
|
+
"kappa3_ratio",
|
211
199
|
"z_score",
|
212
200
|
"skew",
|
213
201
|
"kurtosis",
|
@@ -238,6 +226,7 @@ class PropertiesList(list[str]):
|
|
238
226
|
"downside_deviation",
|
239
227
|
"ret_vol_ratio",
|
240
228
|
"sortino_ratio",
|
229
|
+
"kappa3_ratio",
|
241
230
|
"omega_ratio",
|
242
231
|
"z_score",
|
243
232
|
"skew",
|
openseries/portfoliotools.py
CHANGED
@@ -7,7 +7,7 @@ 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="
|
10
|
+
# mypy: disable-error-code="assignment"
|
11
11
|
from __future__ import annotations
|
12
12
|
|
13
13
|
from inspect import stack
|
@@ -320,7 +320,7 @@ def efficient_frontier(
|
|
320
320
|
|
321
321
|
if tweak:
|
322
322
|
limit_tweak = 0.001
|
323
|
-
line_df["stdev_diff"] = line_df.stdev.
|
323
|
+
line_df["stdev_diff"] = line_df.stdev.pct_change()
|
324
324
|
line_df = line_df.loc[line_df.stdev_diff.abs() > limit_tweak]
|
325
325
|
line_df = line_df.drop(columns="stdev_diff")
|
326
326
|
|
@@ -378,9 +378,11 @@ def constrain_optimized_portfolios(
|
|
378
378
|
condition_least_ret = front_frame.ret > serie.arithmetic_ret
|
379
379
|
# noinspection PyArgumentList
|
380
380
|
least_ret_frame = front_frame[condition_least_ret].sort_values(by="stdev")
|
381
|
-
least_ret_port = least_ret_frame.iloc[0]
|
381
|
+
least_ret_port: Series[float] = least_ret_frame.iloc[0]
|
382
382
|
least_ret_port_name = f"Minimize vol & target return of {portfolioname}"
|
383
|
-
least_ret_weights
|
383
|
+
least_ret_weights: list[float] = [
|
384
|
+
least_ret_port.loc[c] for c in lr_frame.columns_lvl_zero
|
385
|
+
]
|
384
386
|
lr_frame.weights = least_ret_weights
|
385
387
|
resleast = OpenTimeSeries.from_df(lr_frame.make_portfolio(least_ret_port_name))
|
386
388
|
|
@@ -390,9 +392,11 @@ def constrain_optimized_portfolios(
|
|
390
392
|
by="ret",
|
391
393
|
ascending=False,
|
392
394
|
)
|
393
|
-
most_vol_port = most_vol_frame.iloc[0]
|
395
|
+
most_vol_port: Series[float] = most_vol_frame.iloc[0]
|
394
396
|
most_vol_port_name = f"Maximize return & target risk of {portfolioname}"
|
395
|
-
most_vol_weights
|
397
|
+
most_vol_weights: list[float] = [
|
398
|
+
most_vol_port.loc[c] for c in mv_frame.columns_lvl_zero
|
399
|
+
]
|
396
400
|
mv_frame.weights = most_vol_weights
|
397
401
|
resmost = OpenTimeSeries.from_df(mv_frame.make_portfolio(most_vol_port_name))
|
398
402
|
|
@@ -562,7 +566,7 @@ def sharpeplot(
|
|
562
566
|
)
|
563
567
|
|
564
568
|
if point_frame is not None:
|
565
|
-
colorway = cast(
|
569
|
+
colorway = cast( # type: ignore[index]
|
566
570
|
"dict[str, str | int | float | bool | list[str]]",
|
567
571
|
fig["layout"],
|
568
572
|
).get("colorway")[: len(point_frame.columns)]
|