openseries 1.7.3__py3-none-any.whl → 1.7.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 +41 -12
- openseries/frame.py +109 -72
- openseries/portfoliotools.py +13 -9
- openseries/series.py +12 -18
- openseries/simulation.py +7 -4
- {openseries-1.7.3.dist-info → openseries-1.7.5.dist-info}/METADATA +3 -3
- openseries-1.7.5.dist-info/RECORD +16 -0
- openseries-1.7.3.dist-info/RECORD +0 -16
- {openseries-1.7.3.dist-info → openseries-1.7.5.dist-info}/LICENSE.md +0 -0
- {openseries-1.7.3.dist-info → openseries-1.7.5.dist-info}/WHEEL +0 -0
openseries/_common_model.py
CHANGED
@@ -59,6 +59,7 @@ from .types import (
|
|
59
59
|
)
|
60
60
|
|
61
61
|
|
62
|
+
# noinspection PyTypeChecker
|
62
63
|
class _CommonModel(BaseModel):
|
63
64
|
"""Declare _CommonModel."""
|
64
65
|
|
@@ -359,7 +360,7 @@ class _CommonModel(BaseModel):
|
|
359
360
|
"""
|
360
361
|
wmdf = self.tsdf.copy()
|
361
362
|
wmdf.index = DatetimeIndex(wmdf.index)
|
362
|
-
result = wmdf.resample("BME").last().pct_change().min()
|
363
|
+
result = wmdf.resample("BME").last().ffill().pct_change().min()
|
363
364
|
|
364
365
|
if self.tsdf.shape[1] == 1:
|
365
366
|
return float(result.iloc[0])
|
@@ -680,7 +681,7 @@ class _CommonModel(BaseModel):
|
|
680
681
|
output.append(dict(itemdata))
|
681
682
|
|
682
683
|
with dirpath.joinpath(filename).open(mode="w", encoding="utf-8") as jsonfile:
|
683
|
-
dump(output, jsonfile, indent=2, sort_keys=False)
|
684
|
+
dump(obj=output, fp=jsonfile, indent=2, sort_keys=False)
|
684
685
|
|
685
686
|
return output
|
686
687
|
|
@@ -1027,7 +1028,10 @@ class _CommonModel(BaseModel):
|
|
1027
1028
|
time_factor = how_many / fraction
|
1028
1029
|
|
1029
1030
|
result = (
|
1030
|
-
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1031
|
+
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1032
|
+
.ffill()
|
1033
|
+
.pct_change()
|
1034
|
+
.mean()
|
1031
1035
|
* time_factor
|
1032
1036
|
)
|
1033
1037
|
|
@@ -1085,7 +1089,7 @@ class _CommonModel(BaseModel):
|
|
1085
1089
|
time_factor = how_many / fraction
|
1086
1090
|
|
1087
1091
|
data = self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1088
|
-
result = data.pct_change().std().mul(sqrt(time_factor))
|
1092
|
+
result = data.ffill().pct_change().std().mul(sqrt(time_factor))
|
1089
1093
|
|
1090
1094
|
if self.tsdf.shape[1] == 1:
|
1091
1095
|
return float(cast(SupportsFloat, result.iloc[0]))
|
@@ -1281,19 +1285,24 @@ class _CommonModel(BaseModel):
|
|
1281
1285
|
if drift_adjust:
|
1282
1286
|
imp_vol = (-sqrt(time_factor) / norm.ppf(level)) * (
|
1283
1287
|
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1288
|
+
.ffill()
|
1284
1289
|
.pct_change()
|
1285
1290
|
.quantile(1 - level, interpolation=interpolation)
|
1286
1291
|
- self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1292
|
+
.ffill()
|
1287
1293
|
.pct_change()
|
1288
1294
|
.sum()
|
1289
1295
|
/ len(
|
1290
|
-
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1296
|
+
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1297
|
+
.ffill()
|
1298
|
+
.pct_change(),
|
1291
1299
|
)
|
1292
1300
|
)
|
1293
1301
|
else:
|
1294
1302
|
imp_vol = (
|
1295
1303
|
-sqrt(time_factor)
|
1296
1304
|
* self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1305
|
+
.ffill()
|
1297
1306
|
.pct_change()
|
1298
1307
|
.quantile(1 - level, interpolation=interpolation)
|
1299
1308
|
/ norm.ppf(level)
|
@@ -1357,6 +1366,7 @@ class _CommonModel(BaseModel):
|
|
1357
1366
|
cvar_df = self.tsdf.loc[cast(int, earlier) : cast(int, later)].copy(deep=True)
|
1358
1367
|
result = [
|
1359
1368
|
cvar_df.loc[:, x] # type: ignore[call-overload,index]
|
1369
|
+
.ffill()
|
1360
1370
|
.pct_change()
|
1361
1371
|
.sort_values()
|
1362
1372
|
.iloc[
|
@@ -1364,6 +1374,7 @@ class _CommonModel(BaseModel):
|
|
1364
1374
|
ceil(
|
1365
1375
|
(1 - level)
|
1366
1376
|
* cvar_df.loc[:, x] # type: ignore[index]
|
1377
|
+
.ffill()
|
1367
1378
|
.pct_change()
|
1368
1379
|
.count(),
|
1369
1380
|
),
|
@@ -1424,6 +1435,7 @@ class _CommonModel(BaseModel):
|
|
1424
1435
|
)
|
1425
1436
|
how_many = (
|
1426
1437
|
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1438
|
+
.ffill()
|
1427
1439
|
.pct_change()
|
1428
1440
|
.count(numeric_only=True)
|
1429
1441
|
)
|
@@ -1439,6 +1451,7 @@ class _CommonModel(BaseModel):
|
|
1439
1451
|
|
1440
1452
|
dddf = (
|
1441
1453
|
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1454
|
+
.ffill()
|
1442
1455
|
.pct_change()
|
1443
1456
|
.sub(min_accepted_return / time_factor)
|
1444
1457
|
)
|
@@ -1542,6 +1555,7 @@ class _CommonModel(BaseModel):
|
|
1542
1555
|
)
|
1543
1556
|
result: NDArray[float64] = skew(
|
1544
1557
|
a=self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1558
|
+
.ffill()
|
1545
1559
|
.pct_change()
|
1546
1560
|
.to_numpy(),
|
1547
1561
|
bias=True,
|
@@ -1589,7 +1603,7 @@ class _CommonModel(BaseModel):
|
|
1589
1603
|
to_dt=to_date,
|
1590
1604
|
)
|
1591
1605
|
result: NDArray[float64] = kurtosis(
|
1592
|
-
self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(),
|
1606
|
+
self.tsdf.loc[cast(int, earlier) : cast(int, later)].ffill().pct_change(),
|
1593
1607
|
fisher=True,
|
1594
1608
|
bias=True,
|
1595
1609
|
nan_policy="omit",
|
@@ -1685,13 +1699,21 @@ class _CommonModel(BaseModel):
|
|
1685
1699
|
)
|
1686
1700
|
pos = (
|
1687
1701
|
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1702
|
+
.ffill()
|
1688
1703
|
.pct_change()[1:][
|
1689
|
-
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1704
|
+
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1705
|
+
.ffill()
|
1706
|
+
.pct_change()[1:]
|
1690
1707
|
> zero
|
1691
1708
|
]
|
1692
1709
|
.count()
|
1693
1710
|
)
|
1694
|
-
tot =
|
1711
|
+
tot = (
|
1712
|
+
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1713
|
+
.ffill()
|
1714
|
+
.pct_change()
|
1715
|
+
.count()
|
1716
|
+
)
|
1695
1717
|
share = pos / tot
|
1696
1718
|
if self.tsdf.shape[1] == 1:
|
1697
1719
|
return float(share.iloc[0])
|
@@ -1867,7 +1889,9 @@ class _CommonModel(BaseModel):
|
|
1867
1889
|
from_dt=from_date,
|
1868
1890
|
to_dt=to_date,
|
1869
1891
|
)
|
1870
|
-
retdf =
|
1892
|
+
retdf = (
|
1893
|
+
self.tsdf.loc[cast(int, earlier) : cast(int, later)].ffill().pct_change()
|
1894
|
+
)
|
1871
1895
|
pos = retdf[retdf > min_accepted_return].sub(min_accepted_return).sum()
|
1872
1896
|
neg = retdf[retdf < min_accepted_return].sub(min_accepted_return).sum()
|
1873
1897
|
ratio = pos / -neg
|
@@ -1955,7 +1979,7 @@ class _CommonModel(BaseModel):
|
|
1955
1979
|
period = "-".join([str(year), str(month).zfill(2)])
|
1956
1980
|
vrdf = self.tsdf.copy()
|
1957
1981
|
vrdf.index = DatetimeIndex(vrdf.index)
|
1958
|
-
resultdf = DataFrame(vrdf.pct_change())
|
1982
|
+
resultdf = DataFrame(vrdf.ffill().pct_change())
|
1959
1983
|
result = resultdf.loc[period] + 1
|
1960
1984
|
cal_period = result.cumprod(axis="index").iloc[-1] - 1
|
1961
1985
|
if self.tsdf.shape[1] == 1:
|
@@ -2007,6 +2031,7 @@ class _CommonModel(BaseModel):
|
|
2007
2031
|
)
|
2008
2032
|
result = (
|
2009
2033
|
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
2034
|
+
.ffill()
|
2010
2035
|
.pct_change()
|
2011
2036
|
.quantile(1 - level, interpolation=interpolation)
|
2012
2037
|
)
|
@@ -2055,6 +2080,7 @@ class _CommonModel(BaseModel):
|
|
2055
2080
|
)
|
2056
2081
|
result = (
|
2057
2082
|
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
2083
|
+
.ffill()
|
2058
2084
|
.pct_change()
|
2059
2085
|
.rolling(observations, min_periods=observations)
|
2060
2086
|
.sum()
|
@@ -2101,7 +2127,9 @@ class _CommonModel(BaseModel):
|
|
2101
2127
|
from_dt=from_date,
|
2102
2128
|
to_dt=to_date,
|
2103
2129
|
)
|
2104
|
-
zscframe =
|
2130
|
+
zscframe = (
|
2131
|
+
self.tsdf.loc[cast(int, earlier) : cast(int, later)].ffill().pct_change()
|
2132
|
+
)
|
2105
2133
|
result = (zscframe.iloc[-1] - zscframe.mean()) / zscframe.std()
|
2106
2134
|
|
2107
2135
|
if self.tsdf.shape[1] == 1:
|
@@ -2170,6 +2198,7 @@ class _CommonModel(BaseModel):
|
|
2170
2198
|
ret_label = cast(tuple[str], self.tsdf.iloc[:, column].name)[0]
|
2171
2199
|
retseries = (
|
2172
2200
|
self.tsdf.iloc[:, column]
|
2201
|
+
.ffill()
|
2173
2202
|
.pct_change()
|
2174
2203
|
.rolling(observations, min_periods=observations)
|
2175
2204
|
.sum()
|
@@ -2247,7 +2276,7 @@ class _CommonModel(BaseModel):
|
|
2247
2276
|
else:
|
2248
2277
|
time_factor = self.periods_in_a_year
|
2249
2278
|
vol_label = cast(tuple[str, ValueType], self.tsdf.iloc[:, column].name)[0]
|
2250
|
-
dframe = self.tsdf.iloc[:, column].pct_change()
|
2279
|
+
dframe = self.tsdf.iloc[:, column].ffill().pct_change()
|
2251
2280
|
volseries = dframe.rolling(
|
2252
2281
|
observations,
|
2253
2282
|
min_periods=observations,
|
openseries/frame.py
CHANGED
@@ -14,7 +14,6 @@ if TYPE_CHECKING:
|
|
14
14
|
import statsmodels.api as sm # type: ignore[import-untyped,unused-ignore]
|
15
15
|
from numpy import (
|
16
16
|
cov,
|
17
|
-
cumprod,
|
18
17
|
divide,
|
19
18
|
isinf,
|
20
19
|
log,
|
@@ -336,11 +335,12 @@ class OpenFrame(_CommonModel):
|
|
336
335
|
The returns of the values in the series
|
337
336
|
|
338
337
|
"""
|
339
|
-
|
340
|
-
|
338
|
+
returns = self.tsdf.ffill().pct_change()
|
339
|
+
returns.iloc[0] = 0
|
341
340
|
new_labels = [ValueType.RTRN] * self.item_count
|
342
341
|
arrays = [self.tsdf.columns.get_level_values(0), new_labels]
|
343
|
-
|
342
|
+
returns.columns = MultiIndex.from_arrays(arrays=arrays)
|
343
|
+
self.tsdf = returns.copy()
|
344
344
|
return self
|
345
345
|
|
346
346
|
def value_to_diff(self: Self, periods: int = 1) -> Self:
|
@@ -374,14 +374,20 @@ class OpenFrame(_CommonModel):
|
|
374
374
|
An OpenFrame object
|
375
375
|
|
376
376
|
"""
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
377
|
+
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
378
|
+
if not any(vtypes):
|
379
|
+
returns = self.tsdf.ffill().pct_change()
|
380
|
+
returns.iloc[0] = 0
|
381
|
+
elif all(vtypes):
|
382
|
+
returns = self.tsdf.copy()
|
383
|
+
returns.iloc[0] = 0
|
384
|
+
else:
|
385
|
+
msg = "Mix of series types will give inconsistent results"
|
386
|
+
raise ValueError(msg)
|
387
|
+
|
388
|
+
returns = returns.add(1.0)
|
389
|
+
self.tsdf = returns.cumprod(axis=0) / returns.iloc[0]
|
382
390
|
|
383
|
-
self.tsdf = self.tsdf.add(1.0)
|
384
|
-
self.tsdf = self.tsdf.apply(cumprod, axis="index") / self.tsdf.iloc[0]
|
385
391
|
new_labels = [ValueType.PRICE] * self.item_count
|
386
392
|
arrays = [self.tsdf.columns.get_level_values(0), new_labels]
|
387
393
|
self.tsdf.columns = MultiIndex.from_arrays(arrays)
|
@@ -453,8 +459,15 @@ class OpenFrame(_CommonModel):
|
|
453
459
|
method=method,
|
454
460
|
)
|
455
461
|
|
462
|
+
arrays = [
|
463
|
+
self.tsdf.columns.get_level_values(0),
|
464
|
+
self.tsdf.columns.get_level_values(1),
|
465
|
+
]
|
466
|
+
|
456
467
|
self._set_tsdf()
|
457
468
|
|
469
|
+
self.tsdf.columns = MultiIndex.from_arrays(arrays)
|
470
|
+
|
458
471
|
return self
|
459
472
|
|
460
473
|
def ewma_risk(
|
@@ -589,9 +602,13 @@ class OpenFrame(_CommonModel):
|
|
589
602
|
Correlation matrix
|
590
603
|
|
591
604
|
"""
|
592
|
-
corr_matrix =
|
593
|
-
|
594
|
-
|
605
|
+
corr_matrix = (
|
606
|
+
self.tsdf.ffill()
|
607
|
+
.pct_change()
|
608
|
+
.corr(
|
609
|
+
method="pearson",
|
610
|
+
min_periods=1,
|
611
|
+
)
|
595
612
|
)
|
596
613
|
corr_matrix.columns = corr_matrix.columns.droplevel(level=1)
|
597
614
|
corr_matrix.index = corr_matrix.index.droplevel(level=1)
|
@@ -816,7 +833,7 @@ class OpenFrame(_CommonModel):
|
|
816
833
|
]
|
817
834
|
relative = 1.0 + longdf - shortdf
|
818
835
|
vol = float(
|
819
|
-
relative.pct_change().std() * sqrt(time_factor),
|
836
|
+
relative.ffill().pct_change().std() * sqrt(time_factor),
|
820
837
|
)
|
821
838
|
terrors.append(vol)
|
822
839
|
|
@@ -906,10 +923,10 @@ class OpenFrame(_CommonModel):
|
|
906
923
|
]
|
907
924
|
relative = 1.0 + longdf - shortdf
|
908
925
|
ret = float(
|
909
|
-
relative.pct_change().mean() * time_factor,
|
926
|
+
relative.ffill().pct_change().mean() * time_factor,
|
910
927
|
)
|
911
928
|
vol = float(
|
912
|
-
relative.pct_change().std() * sqrt(time_factor),
|
929
|
+
relative.ffill().pct_change().std() * sqrt(time_factor),
|
913
930
|
)
|
914
931
|
ratios.append(ret / vol)
|
915
932
|
|
@@ -1008,16 +1025,18 @@ class OpenFrame(_CommonModel):
|
|
1008
1025
|
msg = "ratio must be one of 'up', 'down' or 'both'."
|
1009
1026
|
if ratio == "up":
|
1010
1027
|
uparray = (
|
1011
|
-
longdf.
|
1012
|
-
|
1028
|
+
longdf.ffill()
|
1029
|
+
.pct_change()[
|
1030
|
+
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1013
1031
|
]
|
1014
1032
|
.add(1)
|
1015
1033
|
.to_numpy()
|
1016
1034
|
)
|
1017
1035
|
up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
|
1018
1036
|
upidxarray = (
|
1019
|
-
shortdf.
|
1020
|
-
|
1037
|
+
shortdf.ffill()
|
1038
|
+
.pct_change()[
|
1039
|
+
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1021
1040
|
]
|
1022
1041
|
.add(1)
|
1023
1042
|
.to_numpy()
|
@@ -1028,8 +1047,9 @@ class OpenFrame(_CommonModel):
|
|
1028
1047
|
ratios.append(up_rtrn / up_idx_return)
|
1029
1048
|
elif ratio == "down":
|
1030
1049
|
downarray = (
|
1031
|
-
longdf.
|
1032
|
-
|
1050
|
+
longdf.ffill()
|
1051
|
+
.pct_change()[
|
1052
|
+
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1033
1053
|
]
|
1034
1054
|
.add(1)
|
1035
1055
|
.to_numpy()
|
@@ -1038,8 +1058,9 @@ class OpenFrame(_CommonModel):
|
|
1038
1058
|
downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
|
1039
1059
|
)
|
1040
1060
|
downidxarray = (
|
1041
|
-
shortdf.
|
1042
|
-
|
1061
|
+
shortdf.ffill()
|
1062
|
+
.pct_change()[
|
1063
|
+
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1043
1064
|
]
|
1044
1065
|
.add(1)
|
1045
1066
|
.to_numpy()
|
@@ -1051,16 +1072,18 @@ class OpenFrame(_CommonModel):
|
|
1051
1072
|
ratios.append(down_return / down_idx_return)
|
1052
1073
|
elif ratio == "both":
|
1053
1074
|
uparray = (
|
1054
|
-
longdf.
|
1055
|
-
|
1075
|
+
longdf.ffill()
|
1076
|
+
.pct_change()[
|
1077
|
+
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1056
1078
|
]
|
1057
1079
|
.add(1)
|
1058
1080
|
.to_numpy()
|
1059
1081
|
)
|
1060
1082
|
up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
|
1061
1083
|
upidxarray = (
|
1062
|
-
shortdf.
|
1063
|
-
|
1084
|
+
shortdf.ffill()
|
1085
|
+
.pct_change()[
|
1086
|
+
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1064
1087
|
]
|
1065
1088
|
.add(1)
|
1066
1089
|
.to_numpy()
|
@@ -1069,8 +1092,9 @@ class OpenFrame(_CommonModel):
|
|
1069
1092
|
upidxarray.prod() ** (1 / (len(upidxarray) / time_factor)) - 1
|
1070
1093
|
)
|
1071
1094
|
downarray = (
|
1072
|
-
longdf.
|
1073
|
-
|
1095
|
+
longdf.ffill()
|
1096
|
+
.pct_change()[
|
1097
|
+
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1074
1098
|
]
|
1075
1099
|
.add(1)
|
1076
1100
|
.to_numpy()
|
@@ -1079,8 +1103,9 @@ class OpenFrame(_CommonModel):
|
|
1079
1103
|
downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
|
1080
1104
|
)
|
1081
1105
|
downidxarray = (
|
1082
|
-
shortdf.
|
1083
|
-
|
1106
|
+
shortdf.ffill()
|
1107
|
+
.pct_change()[
|
1108
|
+
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1084
1109
|
]
|
1085
1110
|
.add(1)
|
1086
1111
|
.to_numpy()
|
@@ -1284,30 +1309,8 @@ class OpenFrame(_CommonModel):
|
|
1284
1309
|
|
1285
1310
|
"""
|
1286
1311
|
full_year = 1.0
|
1287
|
-
|
1288
|
-
|
1289
|
-
for x in self.tsdf.columns.get_level_values(1).to_numpy()
|
1290
|
-
):
|
1291
|
-
msg = "asset should be a tuple[str, ValueType] or an integer."
|
1292
|
-
if isinstance(asset, tuple):
|
1293
|
-
asset_log = self.tsdf.loc[:, asset]
|
1294
|
-
asset_cagr = asset_log.mean()
|
1295
|
-
elif isinstance(asset, int):
|
1296
|
-
asset_log = self.tsdf.iloc[:, asset]
|
1297
|
-
asset_cagr = asset_log.mean()
|
1298
|
-
else:
|
1299
|
-
raise TypeError(msg)
|
1300
|
-
|
1301
|
-
msg = "market should be a tuple[str, ValueType] or an integer."
|
1302
|
-
if isinstance(market, tuple):
|
1303
|
-
market_log = self.tsdf.loc[:, market]
|
1304
|
-
market_cagr = market_log.mean()
|
1305
|
-
elif isinstance(market, int):
|
1306
|
-
market_log = self.tsdf.iloc[:, market]
|
1307
|
-
market_cagr = market_log.mean()
|
1308
|
-
else:
|
1309
|
-
raise TypeError(msg)
|
1310
|
-
else:
|
1312
|
+
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
1313
|
+
if not any(vtypes):
|
1311
1314
|
msg = "asset should be a tuple[str, ValueType] or an integer."
|
1312
1315
|
if isinstance(asset, tuple):
|
1313
1316
|
asset_log = log(
|
@@ -1375,6 +1378,29 @@ class OpenFrame(_CommonModel):
|
|
1375
1378
|
)
|
1376
1379
|
else:
|
1377
1380
|
raise TypeError(msg)
|
1381
|
+
elif all(vtypes):
|
1382
|
+
msg = "asset should be a tuple[str, ValueType] or an integer."
|
1383
|
+
if isinstance(asset, tuple):
|
1384
|
+
asset_log = self.tsdf.loc[:, asset]
|
1385
|
+
asset_cagr = asset_log.mean()
|
1386
|
+
elif isinstance(asset, int):
|
1387
|
+
asset_log = self.tsdf.iloc[:, asset]
|
1388
|
+
asset_cagr = asset_log.mean()
|
1389
|
+
else:
|
1390
|
+
raise TypeError(msg)
|
1391
|
+
|
1392
|
+
msg = "market should be a tuple[str, ValueType] or an integer."
|
1393
|
+
if isinstance(market, tuple):
|
1394
|
+
market_log = self.tsdf.loc[:, market]
|
1395
|
+
market_cagr = market_log.mean()
|
1396
|
+
elif isinstance(market, int):
|
1397
|
+
market_log = self.tsdf.iloc[:, market]
|
1398
|
+
market_cagr = market_log.mean()
|
1399
|
+
else:
|
1400
|
+
raise TypeError(msg)
|
1401
|
+
else:
|
1402
|
+
msg = "Mix of series types will give inconsistent results"
|
1403
|
+
raise ValueError(msg)
|
1378
1404
|
|
1379
1405
|
covariance = cov(asset_log, market_log, ddof=dlta_degr_freedms)
|
1380
1406
|
beta = covariance[0, 1] / covariance[1, 1]
|
@@ -1407,27 +1433,30 @@ class OpenFrame(_CommonModel):
|
|
1407
1433
|
"to run the make_portfolio method."
|
1408
1434
|
)
|
1409
1435
|
raise ValueError(msg)
|
1410
|
-
|
1411
|
-
|
1412
|
-
|
1413
|
-
|
1414
|
-
|
1415
|
-
|
1416
|
-
|
1436
|
+
|
1437
|
+
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
1438
|
+
if not any(vtypes):
|
1439
|
+
returns = self.tsdf.ffill().pct_change()
|
1440
|
+
returns.iloc[0] = 0
|
1441
|
+
elif all(vtypes):
|
1442
|
+
returns = self.tsdf.copy()
|
1443
|
+
else:
|
1444
|
+
msg = "Mix of series types will give inconsistent results"
|
1445
|
+
raise ValueError(msg)
|
1417
1446
|
|
1418
1447
|
msg = "Weight strategy not implemented"
|
1419
1448
|
if weight_strat:
|
1420
1449
|
if weight_strat == "eq_weights":
|
1421
1450
|
self.weights = [1.0 / self.item_count] * self.item_count
|
1422
1451
|
elif weight_strat == "inv_vol":
|
1423
|
-
vol = divide(1.0, std(
|
1452
|
+
vol = divide(1.0, std(returns, axis=0, ddof=1))
|
1424
1453
|
vol[isinf(vol)] = nan
|
1425
1454
|
self.weights = list(divide(vol, vol.sum()))
|
1426
1455
|
else:
|
1427
1456
|
raise NotImplementedError(msg)
|
1428
1457
|
|
1429
1458
|
return DataFrame(
|
1430
|
-
data=(
|
1459
|
+
data=(returns @ self.weights).add(1.0).cumprod(),
|
1431
1460
|
index=self.tsdf.index,
|
1432
1461
|
columns=[[name], [ValueType.PRICE]],
|
1433
1462
|
dtype="float64",
|
@@ -1482,11 +1511,14 @@ class OpenFrame(_CommonModel):
|
|
1482
1511
|
)
|
1483
1512
|
|
1484
1513
|
retseries = (
|
1485
|
-
relative.
|
1514
|
+
relative.ffill()
|
1515
|
+
.pct_change()
|
1516
|
+
.rolling(observations, min_periods=observations)
|
1517
|
+
.sum()
|
1486
1518
|
)
|
1487
1519
|
retdf = retseries.dropna().to_frame()
|
1488
1520
|
|
1489
|
-
voldf = relative.pct_change().rolling(
|
1521
|
+
voldf = relative.ffill().pct_change().rolling(
|
1490
1522
|
observations,
|
1491
1523
|
min_periods=observations,
|
1492
1524
|
).std() * sqrt(time_factor)
|
@@ -1530,9 +1562,13 @@ class OpenFrame(_CommonModel):
|
|
1530
1562
|
asset_label = cast(tuple[str, str], self.tsdf.iloc[:, asset_column].name)[0]
|
1531
1563
|
beta_label = f"{asset_label} / {market_label}"
|
1532
1564
|
|
1533
|
-
rolling =
|
1534
|
-
|
1535
|
-
|
1565
|
+
rolling = (
|
1566
|
+
self.tsdf.ffill()
|
1567
|
+
.pct_change()
|
1568
|
+
.rolling(
|
1569
|
+
observations,
|
1570
|
+
min_periods=observations,
|
1571
|
+
)
|
1536
1572
|
)
|
1537
1573
|
|
1538
1574
|
rcov = rolling.cov(ddof=dlta_degr_freedms)
|
@@ -1587,10 +1623,11 @@ class OpenFrame(_CommonModel):
|
|
1587
1623
|
)
|
1588
1624
|
first_series = (
|
1589
1625
|
self.tsdf.iloc[:, first_column]
|
1626
|
+
.ffill()
|
1590
1627
|
.pct_change()[1:]
|
1591
1628
|
.rolling(observations, min_periods=observations)
|
1592
1629
|
)
|
1593
|
-
second_series = self.tsdf.iloc[:, second_column].pct_change()[1:]
|
1630
|
+
second_series = self.tsdf.iloc[:, second_column].ffill().pct_change()[1:]
|
1594
1631
|
corrdf = first_series.corr(other=second_series).dropna().to_frame()
|
1595
1632
|
corrdf.columns = MultiIndex.from_arrays(
|
1596
1633
|
[
|
openseries/portfoliotools.py
CHANGED
@@ -83,13 +83,15 @@ def simulate_portfolios(
|
|
83
83
|
"""
|
84
84
|
copi = simframe.from_deepcopy()
|
85
85
|
|
86
|
-
|
87
|
-
|
88
|
-
):
|
86
|
+
vtypes = [x == ValueType.RTRN for x in copi.tsdf.columns.get_level_values(1)]
|
87
|
+
if not any(vtypes):
|
89
88
|
copi.value_to_ret()
|
90
89
|
log_ret = copi.tsdf.copy()[1:]
|
91
|
-
|
90
|
+
elif all(vtypes):
|
92
91
|
log_ret = copi.tsdf.copy()
|
92
|
+
else:
|
93
|
+
msg = "Mix of series types will give inconsistent results"
|
94
|
+
raise ValueError(msg)
|
93
95
|
|
94
96
|
log_ret.columns = log_ret.columns.droplevel(level=1)
|
95
97
|
|
@@ -165,13 +167,15 @@ def efficient_frontier( # noqa: C901
|
|
165
167
|
|
166
168
|
copi = eframe.from_deepcopy()
|
167
169
|
|
168
|
-
|
169
|
-
|
170
|
-
):
|
170
|
+
vtypes = [x == ValueType.RTRN for x in copi.tsdf.columns.get_level_values(1)]
|
171
|
+
if not any(vtypes):
|
171
172
|
copi.value_to_ret()
|
172
173
|
log_ret = copi.tsdf.copy()[1:]
|
173
|
-
|
174
|
+
elif all(vtypes):
|
174
175
|
log_ret = copi.tsdf.copy()
|
176
|
+
else:
|
177
|
+
msg = "Mix of series types will give inconsistent results"
|
178
|
+
raise ValueError(msg)
|
175
179
|
|
176
180
|
log_ret.columns = log_ret.columns.droplevel(level=1)
|
177
181
|
|
@@ -304,7 +308,7 @@ def efficient_frontier( # noqa: C901
|
|
304
308
|
|
305
309
|
if tweak:
|
306
310
|
limit_tweak = 0.001
|
307
|
-
line_df["stdev_diff"] = line_df.stdev.pct_change()
|
311
|
+
line_df["stdev_diff"] = line_df.stdev.ffill().pct_change()
|
308
312
|
line_df = line_df.loc[line_df.stdev_diff.abs() > limit_tweak]
|
309
313
|
line_df = line_df.drop(columns="stdev_diff")
|
310
314
|
|
openseries/series.py
CHANGED
@@ -346,6 +346,7 @@ class OpenTimeSeries(_CommonModel):
|
|
346
346
|
- cast(DatetimeIndex, d_range)[:-1]
|
347
347
|
],
|
348
348
|
)
|
349
|
+
# noinspection PyTypeChecker
|
349
350
|
arr = list(cumprod(insert(1 + deltas * rate / 365, 0, 1.0)))
|
350
351
|
dates = [d.strftime("%Y-%m-%d") for d in cast(DatetimeIndex, d_range)]
|
351
352
|
|
@@ -434,15 +435,12 @@ class OpenTimeSeries(_CommonModel):
|
|
434
435
|
The returns of the values in the series
|
435
436
|
|
436
437
|
"""
|
437
|
-
|
438
|
-
|
438
|
+
returns = self.tsdf.ffill().pct_change()
|
439
|
+
returns.iloc[0] = 0
|
439
440
|
self.valuetype = ValueType.RTRN
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
[self.valuetype],
|
444
|
-
],
|
445
|
-
)
|
441
|
+
arrays = [[self.label], [self.valuetype]]
|
442
|
+
returns.columns = MultiIndex.from_arrays(arrays=arrays)
|
443
|
+
self.tsdf = returns.copy()
|
446
444
|
return self
|
447
445
|
|
448
446
|
def value_to_diff(self: Self, periods: int = 1) -> Self:
|
@@ -480,14 +478,12 @@ class OpenTimeSeries(_CommonModel):
|
|
480
478
|
An OpenTimeSeries object
|
481
479
|
|
482
480
|
"""
|
483
|
-
if
|
484
|
-
x == ValueType.RTRN
|
485
|
-
for x in cast(MultiIndex, self.tsdf.columns).get_level_values(1).to_numpy()
|
486
|
-
):
|
481
|
+
if self.valuetype == ValueType.PRICE:
|
487
482
|
self.value_to_ret()
|
488
483
|
|
489
484
|
self.tsdf = self.tsdf.add(1.0)
|
490
485
|
self.tsdf = self.tsdf.cumprod(axis=0) / self.tsdf.iloc[0]
|
486
|
+
|
491
487
|
self.valuetype = ValueType.PRICE
|
492
488
|
self.tsdf.columns = MultiIndex.from_arrays(
|
493
489
|
[
|
@@ -520,6 +516,7 @@ class OpenTimeSeries(_CommonModel):
|
|
520
516
|
arr = array(self.values) / divider
|
521
517
|
|
522
518
|
deltas = array([i.days for i in self.tsdf.index[1:] - self.tsdf.index[:-1]])
|
519
|
+
# noinspection PyTypeChecker
|
523
520
|
arr = cumprod(insert(1.0 + deltas * arr[:-1] / days_in_year, 0, 1.0))
|
524
521
|
|
525
522
|
self.dates = [d.strftime("%Y-%m-%d") for d in self.tsdf.index]
|
@@ -684,17 +681,13 @@ class OpenTimeSeries(_CommonModel):
|
|
684
681
|
An OpenTimeSeries object
|
685
682
|
|
686
683
|
"""
|
687
|
-
|
688
|
-
if any(
|
689
|
-
x == ValueType.RTRN
|
690
|
-
for x in cast(MultiIndex, self.tsdf.columns).get_level_values(1).to_numpy()
|
691
|
-
):
|
684
|
+
if self.valuetype == ValueType.RTRN:
|
692
685
|
ra_df = self.tsdf.copy()
|
693
686
|
values = [1.0]
|
694
687
|
returns_input = True
|
695
688
|
else:
|
696
689
|
values = [cast(float, self.tsdf.iloc[0, 0])]
|
697
|
-
ra_df = self.tsdf.pct_change()
|
690
|
+
ra_df = self.tsdf.ffill().pct_change()
|
698
691
|
returns_input = False
|
699
692
|
ra_df = ra_df.dropna()
|
700
693
|
|
@@ -818,6 +811,7 @@ def timeseries_chain(
|
|
818
811
|
|
819
812
|
dates.extend([x.strftime("%Y-%m-%d") for x in new.tsdf.index])
|
820
813
|
|
814
|
+
# noinspection PyUnresolvedReferences
|
821
815
|
if back.__class__.__subclasscheck__(
|
822
816
|
OpenTimeSeries,
|
823
817
|
):
|
openseries/simulation.py
CHANGED
@@ -122,7 +122,9 @@ class ReturnSimulation(BaseModel):
|
|
122
122
|
"""
|
123
123
|
return cast(
|
124
124
|
float,
|
125
|
-
(
|
125
|
+
(
|
126
|
+
self.results.ffill().pct_change().mean() * self.trading_days_in_year
|
127
|
+
).iloc[0],
|
126
128
|
)
|
127
129
|
|
128
130
|
@property
|
@@ -137,9 +139,10 @@ class ReturnSimulation(BaseModel):
|
|
137
139
|
"""
|
138
140
|
return cast(
|
139
141
|
float,
|
140
|
-
(
|
141
|
-
|
142
|
-
|
142
|
+
(
|
143
|
+
self.results.ffill().pct_change().std()
|
144
|
+
* sqrt(self.trading_days_in_year)
|
145
|
+
).iloc[0],
|
143
146
|
)
|
144
147
|
|
145
148
|
@classmethod
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: openseries
|
3
|
-
Version: 1.7.
|
3
|
+
Version: 1.7.5
|
4
4
|
Summary: Tools for analyzing financial timeseries.
|
5
5
|
Home-page: https://github.com/CaptorAB/openseries
|
6
6
|
License: BSD-3-Clause
|
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
21
21
|
Classifier: Topic :: Office/Business :: Financial :: Investment
|
22
22
|
Requires-Dist: holidays (>=0.30,<1.0)
|
23
|
-
Requires-Dist: numpy (>=1.23.2
|
23
|
+
Requires-Dist: numpy (>=1.23.2,<3.0.0)
|
24
24
|
Requires-Dist: openpyxl (>=3.1.2,<4.0.0)
|
25
25
|
Requires-Dist: pandas (>=2.1.2,<3.0.0)
|
26
26
|
Requires-Dist: plotly (>=5.18.0,<6.0.0)
|
@@ -29,7 +29,7 @@ Requires-Dist: pydantic (>=2.5.2,<3.0.0)
|
|
29
29
|
Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
|
30
30
|
Requires-Dist: requests (>=2.20.0,<3.0.0)
|
31
31
|
Requires-Dist: scipy (>=1.11.4,<2.0.0)
|
32
|
-
Requires-Dist: statsmodels (>=0.14.0,<1.0.0)
|
32
|
+
Requires-Dist: statsmodels (>=0.14.0,!=0.14.2,<1.0.0)
|
33
33
|
Project-URL: Repository, https://github.com/CaptorAB/openseries
|
34
34
|
Description-Content-Type: text/markdown
|
35
35
|
|
@@ -0,0 +1,16 @@
|
|
1
|
+
openseries/__init__.py,sha256=gD2dMKRTJ9HMXLca_5sR67xGiU5sWExwaNUi-9N_RGQ,1032
|
2
|
+
openseries/_common_model.py,sha256=vA950_VY8mD82ZOD2m0J4ndNTUeEdm71kqfZHGJdz1g,73398
|
3
|
+
openseries/_risk.py,sha256=PReIfkzhInvIgJkzI4k1wYvhmLZ4cCurYKuQAvlHLlE,2082
|
4
|
+
openseries/datefixer.py,sha256=3E8Ddf3Ps9k5qUWqL13ONelyMECZsvtyrsFJg7r3bxE,12298
|
5
|
+
openseries/frame.py,sha256=bg9R0KDjSw29sPQClKJ4Cvyj7_O3eVtIGM-ISbD75xk,55360
|
6
|
+
openseries/load_plotly.py,sha256=Uuk_-iIY4_C6Z5U3rAneOh8ZlGYWbkuis9s4Amxzko4,1921
|
7
|
+
openseries/plotly_captor_logo.json,sha256=F5nhMzEyxKywtjvQqMTKgKRCJQYMDIiBgDSxdte8Clo,178
|
8
|
+
openseries/plotly_layouts.json,sha256=ahx8-dL4_RPzvHtBOX0SiL0AH7xQJzNRSDhGrSmU-Og,1429
|
9
|
+
openseries/portfoliotools.py,sha256=s8bQ1SfcXF-xY3GbpyOvMHfypPBXtEl6jDZwVRcrSY4,19102
|
10
|
+
openseries/series.py,sha256=i-zVf3lsk4u_4VlWCSFRQax51LAStMewKIVgiqlo7gQ,27654
|
11
|
+
openseries/simulation.py,sha256=Gc5h3KD3K5AySWqXzm1lbsn2_mwOEWjHr6EVNHb1R-w,13878
|
12
|
+
openseries/types.py,sha256=IbzW9iVuA3MEx3WtjyB6QYeSd3TkGjTOkFnnpSADKlY,7491
|
13
|
+
openseries-1.7.5.dist-info/LICENSE.md,sha256=IQ8_IMXgHxyv4M48G14fJsjcrkiSASdalASTXWCOsj4,1515
|
14
|
+
openseries-1.7.5.dist-info/METADATA,sha256=bWivyL3wdnGRDRJOtjwDYAhkcLkFZT4Bi6_DN7cKydk,43981
|
15
|
+
openseries-1.7.5.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
16
|
+
openseries-1.7.5.dist-info/RECORD,,
|
@@ -1,16 +0,0 @@
|
|
1
|
-
openseries/__init__.py,sha256=gD2dMKRTJ9HMXLca_5sR67xGiU5sWExwaNUi-9N_RGQ,1032
|
2
|
-
openseries/_common_model.py,sha256=t3UeFpNIgWeeGW7lcsVAPF0lsdzNoBDvh__B1TMzigk,72772
|
3
|
-
openseries/_risk.py,sha256=PReIfkzhInvIgJkzI4k1wYvhmLZ4cCurYKuQAvlHLlE,2082
|
4
|
-
openseries/datefixer.py,sha256=3E8Ddf3Ps9k5qUWqL13ONelyMECZsvtyrsFJg7r3bxE,12298
|
5
|
-
openseries/frame.py,sha256=xXBPgKGpuHE2M3P4aHitWiJaeWXE3dbd5oEvxrg_7wY,54065
|
6
|
-
openseries/load_plotly.py,sha256=Uuk_-iIY4_C6Z5U3rAneOh8ZlGYWbkuis9s4Amxzko4,1921
|
7
|
-
openseries/plotly_captor_logo.json,sha256=F5nhMzEyxKywtjvQqMTKgKRCJQYMDIiBgDSxdte8Clo,178
|
8
|
-
openseries/plotly_layouts.json,sha256=ahx8-dL4_RPzvHtBOX0SiL0AH7xQJzNRSDhGrSmU-Og,1429
|
9
|
-
openseries/portfoliotools.py,sha256=pI-vUYKbht2u3dTpQ0MHiME67fP27HcaOCxuayW7v-0,18856
|
10
|
-
openseries/series.py,sha256=IvwyXu9hpsItw6LVPQAJ9YNe_bYCXO9ZyNEleuPCLIo,27766
|
11
|
-
openseries/simulation.py,sha256=jghKf0d2deXK-rqn8BeJ_s3JQVLL9FuE_IW29Kf1_-k,13816
|
12
|
-
openseries/types.py,sha256=IbzW9iVuA3MEx3WtjyB6QYeSd3TkGjTOkFnnpSADKlY,7491
|
13
|
-
openseries-1.7.3.dist-info/LICENSE.md,sha256=IQ8_IMXgHxyv4M48G14fJsjcrkiSASdalASTXWCOsj4,1515
|
14
|
-
openseries-1.7.3.dist-info/METADATA,sha256=HmKHHb3SqFrzGIzkKk-l5LXj3PiXagx5CkWH829gbxo,43973
|
15
|
-
openseries-1.7.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
16
|
-
openseries-1.7.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|