openseries 1.9.2__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 +144 -89
- openseries/load_plotly.py +1 -1
- openseries/owntypes.py +3 -0
- openseries/portfoliotools.py +11 -7
- openseries/report.py +3 -3
- openseries/series.py +6 -6
- openseries/simulation.py +6 -10
- {openseries-1.9.2.dist-info → openseries-1.9.3.dist-info}/METADATA +64 -62
- openseries-1.9.3.dist-info/RECORD +17 -0
- openseries-1.9.2.dist-info/RECORD +0 -17
- {openseries-1.9.2.dist-info → openseries-1.9.3.dist-info}/LICENSE.md +0 -0
- {openseries-1.9.2.dist-info → openseries-1.9.3.dist-info}/WHEEL +0 -0
openseries/frame.py
CHANGED
@@ -7,13 +7,13 @@ 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
|
@@ -26,7 +26,6 @@ from numpy import (
|
|
26
26
|
log,
|
27
27
|
nan,
|
28
28
|
sqrt,
|
29
|
-
square,
|
30
29
|
std,
|
31
30
|
)
|
32
31
|
from pandas import (
|
@@ -127,12 +126,14 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
127
126
|
Object of the class OpenFrame
|
128
127
|
|
129
128
|
"""
|
129
|
+
copied_constituents = [ts.from_deepcopy() for ts in constituents]
|
130
|
+
|
130
131
|
super().__init__( # type: ignore[call-arg]
|
131
|
-
constituents=
|
132
|
+
constituents=copied_constituents,
|
132
133
|
weights=weights,
|
133
134
|
)
|
134
135
|
|
135
|
-
self.constituents =
|
136
|
+
self.constituents = copied_constituents
|
136
137
|
self.weights = weights
|
137
138
|
self._set_tsdf()
|
138
139
|
|
@@ -339,10 +340,13 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
339
340
|
The returns of the values in the series
|
340
341
|
|
341
342
|
"""
|
342
|
-
returns = self.tsdf.
|
343
|
+
returns = self.tsdf.pct_change()
|
343
344
|
returns.iloc[0] = 0
|
344
|
-
new_labels = [ValueType.RTRN] * self.item_count
|
345
|
-
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
|
+
]
|
346
350
|
returns.columns = MultiIndex.from_arrays(arrays=arrays)
|
347
351
|
self.tsdf = returns.copy()
|
348
352
|
return self
|
@@ -364,8 +368,11 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
364
368
|
"""
|
365
369
|
self.tsdf = self.tsdf.diff(periods=periods)
|
366
370
|
self.tsdf.iloc[0] = 0
|
367
|
-
new_labels = [ValueType.RTRN] * self.item_count
|
368
|
-
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
|
+
]
|
369
376
|
self.tsdf.columns = MultiIndex.from_arrays(arrays)
|
370
377
|
return self
|
371
378
|
|
@@ -380,7 +387,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
380
387
|
"""
|
381
388
|
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
382
389
|
if not any(vtypes):
|
383
|
-
returns = self.tsdf.
|
390
|
+
returns = self.tsdf.pct_change()
|
384
391
|
returns.iloc[0] = 0
|
385
392
|
elif all(vtypes):
|
386
393
|
returns = self.tsdf.copy()
|
@@ -392,8 +399,11 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
392
399
|
returns = returns.add(1.0)
|
393
400
|
self.tsdf = returns.cumprod(axis=0) / returns.iloc[0]
|
394
401
|
|
395
|
-
new_labels = [ValueType.PRICE] * self.item_count
|
396
|
-
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
|
+
]
|
397
407
|
self.tsdf.columns = MultiIndex.from_arrays(arrays)
|
398
408
|
return self
|
399
409
|
|
@@ -478,6 +488,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
478
488
|
dlta_degr_freedms: int = 0,
|
479
489
|
first_column: int = 0,
|
480
490
|
second_column: int = 1,
|
491
|
+
corr_scale: float = 2.0,
|
481
492
|
months_from_last: int | None = None,
|
482
493
|
from_date: dt.date | None = None,
|
483
494
|
to_date: dt.date | None = None,
|
@@ -500,6 +511,8 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
500
511
|
Column of first timeseries.
|
501
512
|
second_column: int, default: 1
|
502
513
|
Column of second timeseries.
|
514
|
+
corr_scale: float, default: 2.0
|
515
|
+
Correlation scale factor.
|
503
516
|
months_from_last : int, optional
|
504
517
|
number of months offset as positive integer. Overrides use of from_date
|
505
518
|
and to_date
|
@@ -542,9 +555,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
542
555
|
data = self.tsdf.loc[cast("int", earlier) : cast("int", later)].copy()
|
543
556
|
|
544
557
|
for rtn in cols:
|
545
|
-
data[rtn, ValueType.RTRN] = (
|
546
|
-
data.loc[:, (rtn, ValueType.PRICE)].apply(log).diff()
|
547
|
-
)
|
558
|
+
data[rtn, ValueType.RTRN] = log(data.loc[:, (rtn, ValueType.PRICE)]).diff()
|
548
559
|
|
549
560
|
raw_one = [
|
550
561
|
data.loc[:, (cols[0], ValueType.RTRN)]
|
@@ -565,34 +576,39 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
565
576
|
ddof=dlta_degr_freedms,
|
566
577
|
)[0][1],
|
567
578
|
]
|
568
|
-
raw_corr = [raw_cov[0] / (2 * raw_one[0] * raw_two[0])]
|
569
579
|
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
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
|
591
607
|
|
592
608
|
return DataFrame(
|
593
609
|
index=[*cols, corr_label],
|
594
610
|
columns=data.index,
|
595
|
-
data=[
|
611
|
+
data=[vol1, vol2, corr],
|
596
612
|
).T
|
597
613
|
|
598
614
|
@property
|
@@ -605,13 +621,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
605
621
|
Correlation matrix
|
606
622
|
|
607
623
|
"""
|
608
|
-
corr_matrix = (
|
609
|
-
|
610
|
-
|
611
|
-
.corr(
|
612
|
-
method="pearson",
|
613
|
-
min_periods=1,
|
614
|
-
)
|
624
|
+
corr_matrix = self.tsdf.pct_change().corr(
|
625
|
+
method="pearson",
|
626
|
+
min_periods=1,
|
615
627
|
)
|
616
628
|
corr_matrix.columns = corr_matrix.columns.droplevel(level=1)
|
617
629
|
corr_matrix.index = corr_matrix.index.droplevel(level=1)
|
@@ -838,7 +850,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
838
850
|
]
|
839
851
|
relative = 1.0 + longdf - shortdf
|
840
852
|
vol = float(
|
841
|
-
relative.
|
853
|
+
relative.pct_change().std() * sqrt(time_factor),
|
842
854
|
)
|
843
855
|
terrors.append(vol)
|
844
856
|
|
@@ -930,10 +942,10 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
930
942
|
]
|
931
943
|
relative = 1.0 + longdf - shortdf
|
932
944
|
ret = float(
|
933
|
-
relative.
|
945
|
+
relative.pct_change().mean() * time_factor,
|
934
946
|
)
|
935
947
|
vol = float(
|
936
|
-
relative.
|
948
|
+
relative.pct_change().std() * sqrt(time_factor),
|
937
949
|
)
|
938
950
|
ratios.append(ret / vol)
|
939
951
|
|
@@ -1034,18 +1046,16 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1034
1046
|
msg = "ratio must be one of 'up', 'down' or 'both'."
|
1035
1047
|
if ratio == "up":
|
1036
1048
|
uparray = (
|
1037
|
-
longdf.
|
1038
|
-
|
1039
|
-
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1049
|
+
longdf.pct_change()[
|
1050
|
+
shortdf.pct_change().to_numpy() > loss_limit
|
1040
1051
|
]
|
1041
1052
|
.add(1)
|
1042
1053
|
.to_numpy()
|
1043
1054
|
)
|
1044
1055
|
up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
|
1045
1056
|
upidxarray = (
|
1046
|
-
shortdf.
|
1047
|
-
|
1048
|
-
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1057
|
+
shortdf.pct_change()[
|
1058
|
+
shortdf.pct_change().to_numpy() > loss_limit
|
1049
1059
|
]
|
1050
1060
|
.add(1)
|
1051
1061
|
.to_numpy()
|
@@ -1056,9 +1066,8 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1056
1066
|
ratios.append(up_rtrn / up_idx_return)
|
1057
1067
|
elif ratio == "down":
|
1058
1068
|
downarray = (
|
1059
|
-
longdf.
|
1060
|
-
|
1061
|
-
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1069
|
+
longdf.pct_change()[
|
1070
|
+
shortdf.pct_change().to_numpy() < loss_limit
|
1062
1071
|
]
|
1063
1072
|
.add(1)
|
1064
1073
|
.to_numpy()
|
@@ -1067,9 +1076,8 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1067
1076
|
downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
|
1068
1077
|
)
|
1069
1078
|
downidxarray = (
|
1070
|
-
shortdf.
|
1071
|
-
|
1072
|
-
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1079
|
+
shortdf.pct_change()[
|
1080
|
+
shortdf.pct_change().to_numpy() < loss_limit
|
1073
1081
|
]
|
1074
1082
|
.add(1)
|
1075
1083
|
.to_numpy()
|
@@ -1081,18 +1089,16 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1081
1089
|
ratios.append(down_return / down_idx_return)
|
1082
1090
|
elif ratio == "both":
|
1083
1091
|
uparray = (
|
1084
|
-
longdf.
|
1085
|
-
|
1086
|
-
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1092
|
+
longdf.pct_change()[
|
1093
|
+
shortdf.pct_change().to_numpy() > loss_limit
|
1087
1094
|
]
|
1088
1095
|
.add(1)
|
1089
1096
|
.to_numpy()
|
1090
1097
|
)
|
1091
1098
|
up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
|
1092
1099
|
upidxarray = (
|
1093
|
-
shortdf.
|
1094
|
-
|
1095
|
-
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1100
|
+
shortdf.pct_change()[
|
1101
|
+
shortdf.pct_change().to_numpy() > loss_limit
|
1096
1102
|
]
|
1097
1103
|
.add(1)
|
1098
1104
|
.to_numpy()
|
@@ -1101,9 +1107,8 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1101
1107
|
upidxarray.prod() ** (1 / (len(upidxarray) / time_factor)) - 1
|
1102
1108
|
)
|
1103
1109
|
downarray = (
|
1104
|
-
longdf.
|
1105
|
-
|
1106
|
-
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1110
|
+
longdf.pct_change()[
|
1111
|
+
shortdf.pct_change().to_numpy() < loss_limit
|
1107
1112
|
]
|
1108
1113
|
.add(1)
|
1109
1114
|
.to_numpy()
|
@@ -1112,9 +1117,8 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1112
1117
|
downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
|
1113
1118
|
)
|
1114
1119
|
downidxarray = (
|
1115
|
-
shortdf.
|
1116
|
-
|
1117
|
-
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1120
|
+
shortdf.pct_change()[
|
1121
|
+
shortdf.pct_change().to_numpy() < loss_limit
|
1118
1122
|
]
|
1119
1123
|
.add(1)
|
1120
1124
|
.to_numpy()
|
@@ -1410,7 +1414,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1410
1414
|
msg = "Mix of series types will give inconsistent results"
|
1411
1415
|
raise MixedValuetypesError(msg)
|
1412
1416
|
|
1413
|
-
covariance = cov(asset_log, market_log, ddof=dlta_degr_freedms)
|
1417
|
+
covariance = cov(m=asset_log, y=market_log, ddof=dlta_degr_freedms)
|
1414
1418
|
beta = covariance[0, 1] / covariance[1, 1]
|
1415
1419
|
|
1416
1420
|
return float(asset_cagr - riskfree_rate - beta * (market_cagr - riskfree_rate))
|
@@ -1444,7 +1448,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1444
1448
|
|
1445
1449
|
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
1446
1450
|
if not any(vtypes):
|
1447
|
-
returns = self.tsdf.
|
1451
|
+
returns = self.tsdf.pct_change()
|
1448
1452
|
returns.iloc[0] = 0
|
1449
1453
|
elif all(vtypes):
|
1450
1454
|
returns = self.tsdf.copy()
|
@@ -1519,14 +1523,11 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1519
1523
|
)
|
1520
1524
|
|
1521
1525
|
retseries = (
|
1522
|
-
relative.
|
1523
|
-
.pct_change()
|
1524
|
-
.rolling(observations, min_periods=observations)
|
1525
|
-
.sum()
|
1526
|
+
relative.pct_change().rolling(observations, min_periods=observations).sum()
|
1526
1527
|
)
|
1527
1528
|
retdf = retseries.dropna().to_frame()
|
1528
1529
|
|
1529
|
-
voldf = relative.
|
1530
|
+
voldf = relative.pct_change().rolling(
|
1530
1531
|
observations,
|
1531
1532
|
min_periods=observations,
|
1532
1533
|
).std() * sqrt(time_factor)
|
@@ -1572,13 +1573,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1572
1573
|
asset_label = cast("tuple[str, str]", self.tsdf.iloc[:, asset_column].name)[0]
|
1573
1574
|
beta_label = f"{asset_label} / {market_label}"
|
1574
1575
|
|
1575
|
-
rolling = (
|
1576
|
-
|
1577
|
-
|
1578
|
-
.rolling(
|
1579
|
-
observations,
|
1580
|
-
min_periods=observations,
|
1581
|
-
)
|
1576
|
+
rolling = self.tsdf.pct_change().rolling(
|
1577
|
+
observations,
|
1578
|
+
min_periods=observations,
|
1582
1579
|
)
|
1583
1580
|
|
1584
1581
|
rcov = rolling.cov(ddof=dlta_degr_freedms)
|
@@ -1633,11 +1630,10 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1633
1630
|
)
|
1634
1631
|
first_series = (
|
1635
1632
|
self.tsdf.iloc[:, first_column]
|
1636
|
-
.ffill()
|
1637
1633
|
.pct_change()[1:]
|
1638
1634
|
.rolling(observations, min_periods=observations)
|
1639
1635
|
)
|
1640
|
-
second_series = self.tsdf.iloc[:, second_column].
|
1636
|
+
second_series = self.tsdf.iloc[:, second_column].pct_change()[1:]
|
1641
1637
|
corrdf = first_series.corr(other=second_series).dropna().to_frame()
|
1642
1638
|
corrdf.columns = MultiIndex.from_arrays(
|
1643
1639
|
[
|
@@ -1647,3 +1643,62 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1647
1643
|
)
|
1648
1644
|
|
1649
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
@@ -167,6 +167,7 @@ LiteralSeriesProps = Literal[
|
|
167
167
|
"downside_deviation",
|
168
168
|
"ret_vol_ratio",
|
169
169
|
"sortino_ratio",
|
170
|
+
"kappa3_ratio",
|
170
171
|
"z_score",
|
171
172
|
"skew",
|
172
173
|
"kurtosis",
|
@@ -194,6 +195,7 @@ LiteralFrameProps = Literal[
|
|
194
195
|
"downside_deviation",
|
195
196
|
"ret_vol_ratio",
|
196
197
|
"sortino_ratio",
|
198
|
+
"kappa3_ratio",
|
197
199
|
"z_score",
|
198
200
|
"skew",
|
199
201
|
"kurtosis",
|
@@ -224,6 +226,7 @@ class PropertiesList(list[str]):
|
|
224
226
|
"downside_deviation",
|
225
227
|
"ret_vol_ratio",
|
226
228
|
"sortino_ratio",
|
229
|
+
"kappa3_ratio",
|
227
230
|
"omega_ratio",
|
228
231
|
"z_score",
|
229
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)]
|
openseries/report.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="assignment
|
10
|
+
# mypy: disable-error-code="assignment"
|
11
11
|
from __future__ import annotations
|
12
12
|
|
13
13
|
from inspect import stack
|
@@ -244,7 +244,7 @@ def report_html(
|
|
244
244
|
x=bdf.index,
|
245
245
|
y=bdf.iloc[:, item],
|
246
246
|
hovertemplate="%{y:.2%}<br>%{x}",
|
247
|
-
name=bdf.iloc[:, item].name[0],
|
247
|
+
name=bdf.iloc[:, item].name[0], # type: ignore[index]
|
248
248
|
showlegend=False,
|
249
249
|
row=2,
|
250
250
|
col=1,
|
@@ -265,7 +265,7 @@ def report_html(
|
|
265
265
|
]
|
266
266
|
|
267
267
|
# noinspection PyTypeChecker
|
268
|
-
rpt_df = data.all_properties(properties=properties)
|
268
|
+
rpt_df = data.all_properties(properties=properties) # type: ignore[arg-type]
|
269
269
|
alpha_frame = data.from_deepcopy()
|
270
270
|
alpha_frame.to_cumret()
|
271
271
|
with catch_warnings():
|
openseries/series.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="no-any-return"
|
11
10
|
from __future__ import annotations
|
12
11
|
|
13
12
|
from collections.abc import Iterable
|
@@ -469,7 +468,7 @@ class OpenTimeSeries(_CommonModel):
|
|
469
468
|
The returns of the values in the series
|
470
469
|
|
471
470
|
"""
|
472
|
-
returns = self.tsdf.
|
471
|
+
returns = self.tsdf.pct_change()
|
473
472
|
returns.iloc[0] = 0
|
474
473
|
self.valuetype = ValueType.RTRN
|
475
474
|
arrays = [[self.label], [self.valuetype]]
|
@@ -673,9 +672,9 @@ class OpenTimeSeries(_CommonModel):
|
|
673
672
|
|
674
673
|
data = self.tsdf.loc[cast("int", earlier) : cast("int", later)].copy()
|
675
674
|
|
676
|
-
data[self.label, ValueType.RTRN] = (
|
677
|
-
data.loc[:, self.tsdf.columns.to_numpy()[0]]
|
678
|
-
)
|
675
|
+
data[self.label, ValueType.RTRN] = log(
|
676
|
+
data.loc[:, self.tsdf.columns.to_numpy()[0]]
|
677
|
+
).diff()
|
679
678
|
|
680
679
|
rawdata = [
|
681
680
|
data.loc[:, cast("int", (self.label, ValueType.RTRN))]
|
@@ -726,11 +725,12 @@ class OpenTimeSeries(_CommonModel):
|
|
726
725
|
returns_input = True
|
727
726
|
else:
|
728
727
|
values = [cast("float", self.tsdf.iloc[0, 0])]
|
729
|
-
ra_df = self.tsdf.
|
728
|
+
ra_df = self.tsdf.pct_change()
|
730
729
|
returns_input = False
|
731
730
|
ra_df = ra_df.dropna()
|
732
731
|
|
733
732
|
prev = self.first_idx
|
733
|
+
# noinspection PyTypeChecker
|
734
734
|
dates: list[dt.date] = [prev]
|
735
735
|
|
736
736
|
for idx, row in ra_df.iterrows():
|
openseries/simulation.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="no-any-return"
|
11
10
|
from __future__ import annotations
|
12
11
|
|
13
12
|
from typing import TYPE_CHECKING, cast
|
@@ -116,7 +115,7 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
|
|
116
115
|
Simulation data
|
117
116
|
|
118
117
|
"""
|
119
|
-
return self.dframe.add(1.0).cumprod(axis="columns").T
|
118
|
+
return self.dframe.add(1.0).cumprod(axis="columns").T # type: ignore[no-any-return]
|
120
119
|
|
121
120
|
@property
|
122
121
|
def realized_mean_return(self: Self) -> float:
|
@@ -130,9 +129,7 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
|
|
130
129
|
"""
|
131
130
|
return cast(
|
132
131
|
"float",
|
133
|
-
(
|
134
|
-
self.results.ffill().pct_change().mean() * self.trading_days_in_year
|
135
|
-
).iloc[0],
|
132
|
+
(self.results.pct_change().mean() * self.trading_days_in_year).iloc[0],
|
136
133
|
)
|
137
134
|
|
138
135
|
@property
|
@@ -147,10 +144,9 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
|
|
147
144
|
"""
|
148
145
|
return cast(
|
149
146
|
"float",
|
150
|
-
(
|
151
|
-
|
152
|
-
|
153
|
-
).iloc[0],
|
147
|
+
(self.results.pct_change().std() * sqrt(self.trading_days_in_year)).iloc[
|
148
|
+
0
|
149
|
+
],
|
154
150
|
)
|
155
151
|
|
156
152
|
@classmethod
|
@@ -464,7 +460,7 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
|
|
464
460
|
[ValueType.RTRN],
|
465
461
|
],
|
466
462
|
)
|
467
|
-
return sdf
|
463
|
+
return sdf # type: ignore[no-any-return]
|
468
464
|
|
469
465
|
fdf = DataFrame()
|
470
466
|
for item in range(self.number_of_sims):
|