openseries 1.9.3__tar.gz → 1.9.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openseries-1.9.3 → openseries-1.9.4}/PKG-INFO +1 -1
- {openseries-1.9.3 → openseries-1.9.4}/openseries/_common_model.py +50 -15
- {openseries-1.9.3 → openseries-1.9.4}/openseries/frame.py +124 -133
- {openseries-1.9.3 → openseries-1.9.4}/openseries/owntypes.py +4 -0
- {openseries-1.9.3 → openseries-1.9.4}/openseries/portfoliotools.py +1 -1
- {openseries-1.9.3 → openseries-1.9.4}/openseries/report.py +1 -3
- {openseries-1.9.3 → openseries-1.9.4}/openseries/series.py +19 -5
- {openseries-1.9.3 → openseries-1.9.4}/openseries/simulation.py +7 -4
- {openseries-1.9.3 → openseries-1.9.4}/pyproject.toml +2 -2
- {openseries-1.9.3 → openseries-1.9.4}/LICENSE.md +0 -0
- {openseries-1.9.3 → openseries-1.9.4}/README.md +0 -0
- {openseries-1.9.3 → openseries-1.9.4}/openseries/__init__.py +0 -0
- {openseries-1.9.3 → openseries-1.9.4}/openseries/_risk.py +0 -0
- {openseries-1.9.3 → openseries-1.9.4}/openseries/datefixer.py +0 -0
- {openseries-1.9.3 → openseries-1.9.4}/openseries/load_plotly.py +0 -0
- {openseries-1.9.3 → openseries-1.9.4}/openseries/plotly_captor_logo.json +0 -0
- {openseries-1.9.3 → openseries-1.9.4}/openseries/plotly_layouts.json +0 -0
@@ -25,7 +25,9 @@ from .owntypes import (
|
|
25
25
|
DateAlignmentError,
|
26
26
|
InitialValueZeroError,
|
27
27
|
NumberOfItemsAndLabelsNotSameError,
|
28
|
+
ResampleDataLossError,
|
28
29
|
Self,
|
30
|
+
ValueType,
|
29
31
|
)
|
30
32
|
|
31
33
|
if TYPE_CHECKING: # pragma: no cover
|
@@ -47,7 +49,6 @@ if TYPE_CHECKING: # pragma: no cover
|
|
47
49
|
LiteralPlotlyJSlib,
|
48
50
|
LiteralPlotlyOutput,
|
49
51
|
LiteralQuantileInterp,
|
50
|
-
ValueType,
|
51
52
|
)
|
52
53
|
from openpyxl.utils.dataframe import dataframe_to_rows
|
53
54
|
from openpyxl.workbook.workbook import Workbook
|
@@ -432,7 +433,17 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
432
433
|
|
433
434
|
wmdf = wmdf.reindex(index=[deyt.date() for deyt in dates], method=method)
|
434
435
|
wmdf.index = DatetimeIndex(wmdf.index)
|
435
|
-
|
436
|
+
|
437
|
+
vtypes = [x == ValueType.RTRN for x in wmdf.columns.get_level_values(1)]
|
438
|
+
if any(vtypes):
|
439
|
+
msg = (
|
440
|
+
"Do not run worst_month on return series. The operation will "
|
441
|
+
"pick the last data point in the sparser series. It will not sum "
|
442
|
+
"returns and therefore data will be lost and result will be wrong."
|
443
|
+
)
|
444
|
+
raise ResampleDataLossError(msg)
|
445
|
+
|
446
|
+
result = wmdf.ffill().pct_change().min()
|
436
447
|
|
437
448
|
if self.tsdf.shape[1] == 1:
|
438
449
|
return float(result.iloc[0])
|
@@ -1305,6 +1316,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
1305
1316
|
|
1306
1317
|
result = (
|
1307
1318
|
self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
1319
|
+
.ffill()
|
1308
1320
|
.pct_change()
|
1309
1321
|
.mean()
|
1310
1322
|
* time_factor
|
@@ -1366,7 +1378,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
1366
1378
|
time_factor = how_many / fraction
|
1367
1379
|
|
1368
1380
|
data = self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
1369
|
-
result = data.pct_change().std().mul(sqrt(time_factor))
|
1381
|
+
result = data.ffill().pct_change().std().mul(sqrt(time_factor))
|
1370
1382
|
|
1371
1383
|
if self.tsdf.shape[1] == 1:
|
1372
1384
|
return float(cast("SupportsFloat", result.iloc[0]))
|
@@ -1564,21 +1576,24 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
1564
1576
|
if drift_adjust:
|
1565
1577
|
imp_vol = (-sqrt(time_factor) / norm.ppf(level)) * (
|
1566
1578
|
self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
1579
|
+
.ffill()
|
1567
1580
|
.pct_change()
|
1568
1581
|
.quantile(1 - level, interpolation=interpolation)
|
1569
1582
|
- self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
1583
|
+
.ffill()
|
1570
1584
|
.pct_change()
|
1571
1585
|
.sum()
|
1572
1586
|
/ len(
|
1573
|
-
self.tsdf.loc[
|
1574
|
-
|
1575
|
-
|
1587
|
+
self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
1588
|
+
.ffill()
|
1589
|
+
.pct_change(),
|
1576
1590
|
)
|
1577
1591
|
)
|
1578
1592
|
else:
|
1579
1593
|
imp_vol = (
|
1580
1594
|
-sqrt(time_factor)
|
1581
1595
|
* self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
1596
|
+
.ffill()
|
1582
1597
|
.pct_change()
|
1583
1598
|
.quantile(1 - level, interpolation=interpolation)
|
1584
1599
|
/ norm.ppf(level)
|
@@ -1644,12 +1659,14 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
1644
1659
|
)
|
1645
1660
|
result = [
|
1646
1661
|
cvar_df.loc[:, x] # type: ignore[call-overload,index]
|
1662
|
+
.ffill()
|
1647
1663
|
.pct_change()
|
1648
1664
|
.sort_values()
|
1649
1665
|
.iloc[
|
1650
1666
|
: ceil(
|
1651
1667
|
(1 - level)
|
1652
1668
|
* cvar_df.loc[:, x] # type: ignore[index]
|
1669
|
+
.ffill()
|
1653
1670
|
.pct_change()
|
1654
1671
|
.count(),
|
1655
1672
|
),
|
@@ -1718,6 +1735,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
1718
1735
|
|
1719
1736
|
how_many = (
|
1720
1737
|
self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
1738
|
+
.ffill()
|
1721
1739
|
.pct_change()
|
1722
1740
|
.count(numeric_only=True)
|
1723
1741
|
)
|
@@ -1734,6 +1752,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
1734
1752
|
per_period_mar = min_accepted_return / time_factor
|
1735
1753
|
diff = (
|
1736
1754
|
self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
1755
|
+
.ffill()
|
1737
1756
|
.pct_change()
|
1738
1757
|
.sub(per_period_mar)
|
1739
1758
|
)
|
@@ -1845,6 +1864,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
1845
1864
|
)
|
1846
1865
|
result: NDArray[float64] = skew(
|
1847
1866
|
a=self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
1867
|
+
.ffill()
|
1848
1868
|
.pct_change()
|
1849
1869
|
.to_numpy(),
|
1850
1870
|
bias=True,
|
@@ -1892,7 +1912,11 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
1892
1912
|
to_dt=to_date,
|
1893
1913
|
)
|
1894
1914
|
result: NDArray[float64] = kurtosis(
|
1895
|
-
a=(
|
1915
|
+
a=(
|
1916
|
+
self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
1917
|
+
.ffill()
|
1918
|
+
.pct_change()
|
1919
|
+
),
|
1896
1920
|
fisher=True,
|
1897
1921
|
bias=True,
|
1898
1922
|
nan_policy="omit",
|
@@ -1988,16 +2012,18 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
1988
2012
|
)
|
1989
2013
|
pos = (
|
1990
2014
|
self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
2015
|
+
.ffill()
|
1991
2016
|
.pct_change()[1:][
|
1992
|
-
self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
1993
|
-
|
1994
|
-
]
|
2017
|
+
self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
2018
|
+
.ffill()
|
2019
|
+
.pct_change()[1:]
|
1995
2020
|
> zero
|
1996
2021
|
]
|
1997
2022
|
.count()
|
1998
2023
|
)
|
1999
2024
|
tot = (
|
2000
2025
|
self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
2026
|
+
.ffill()
|
2001
2027
|
.pct_change()
|
2002
2028
|
.count()
|
2003
2029
|
)
|
@@ -2184,7 +2210,11 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
2184
2210
|
from_dt=from_date,
|
2185
2211
|
to_dt=to_date,
|
2186
2212
|
)
|
2187
|
-
retdf =
|
2213
|
+
retdf = (
|
2214
|
+
self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
2215
|
+
.ffill()
|
2216
|
+
.pct_change()
|
2217
|
+
)
|
2188
2218
|
pos = retdf[retdf > min_accepted_return].sub(min_accepted_return).sum()
|
2189
2219
|
neg = retdf[retdf < min_accepted_return].sub(min_accepted_return).sum()
|
2190
2220
|
ratio = pos / -neg
|
@@ -2272,7 +2302,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
2272
2302
|
period = "-".join([str(year), str(month).zfill(2)])
|
2273
2303
|
vrdf = self.tsdf.copy()
|
2274
2304
|
vrdf.index = DatetimeIndex(vrdf.index)
|
2275
|
-
resultdf = DataFrame(vrdf.pct_change())
|
2305
|
+
resultdf = DataFrame(vrdf.ffill().pct_change())
|
2276
2306
|
result = resultdf.loc[period] + 1
|
2277
2307
|
cal_period = result.cumprod(axis="index").iloc[-1] - 1
|
2278
2308
|
if self.tsdf.shape[1] == 1:
|
@@ -2324,6 +2354,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
2324
2354
|
)
|
2325
2355
|
result = (
|
2326
2356
|
self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
2357
|
+
.ffill()
|
2327
2358
|
.pct_change()
|
2328
2359
|
.quantile(1 - level, interpolation=interpolation)
|
2329
2360
|
)
|
@@ -2372,6 +2403,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
2372
2403
|
)
|
2373
2404
|
result = (
|
2374
2405
|
self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
2406
|
+
.ffill()
|
2375
2407
|
.pct_change()
|
2376
2408
|
.rolling(observations, min_periods=observations)
|
2377
2409
|
.sum()
|
@@ -2418,9 +2450,11 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
2418
2450
|
from_dt=from_date,
|
2419
2451
|
to_dt=to_date,
|
2420
2452
|
)
|
2421
|
-
zscframe =
|
2422
|
-
cast("int", earlier) : cast("int", later)
|
2423
|
-
|
2453
|
+
zscframe = (
|
2454
|
+
self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
2455
|
+
.ffill()
|
2456
|
+
.pct_change()
|
2457
|
+
)
|
2424
2458
|
result = (zscframe.iloc[-1] - zscframe.mean()) / zscframe.std()
|
2425
2459
|
|
2426
2460
|
if self.tsdf.shape[1] == 1:
|
@@ -2489,6 +2523,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
2489
2523
|
ret_label = cast("tuple[str]", self.tsdf.iloc[:, column].name)[0]
|
2490
2524
|
retseries = (
|
2491
2525
|
Series(self.tsdf.iloc[:, column])
|
2526
|
+
.ffill()
|
2492
2527
|
.pct_change()
|
2493
2528
|
.rolling(observations, min_periods=observations)
|
2494
2529
|
.sum()
|
@@ -57,6 +57,7 @@ from .owntypes import (
|
|
57
57
|
NoWeightsError,
|
58
58
|
OpenFramePropertiesList,
|
59
59
|
RatioInputError,
|
60
|
+
ResampleDataLossError,
|
60
61
|
Self,
|
61
62
|
ValueType,
|
62
63
|
)
|
@@ -340,7 +341,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
340
341
|
The returns of the values in the series
|
341
342
|
|
342
343
|
"""
|
343
|
-
returns = self.tsdf.pct_change()
|
344
|
+
returns = self.tsdf.ffill().pct_change()
|
344
345
|
returns.iloc[0] = 0
|
345
346
|
new_labels: list[ValueType] = [ValueType.RTRN] * self.item_count
|
346
347
|
arrays: list[Index[Any], list[ValueType]] = [ # type: ignore[type-arg]
|
@@ -387,7 +388,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
387
388
|
"""
|
388
389
|
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
389
390
|
if not any(vtypes):
|
390
|
-
returns = self.tsdf.pct_change()
|
391
|
+
returns = self.tsdf.ffill().pct_change()
|
391
392
|
returns.iloc[0] = 0
|
392
393
|
elif all(vtypes):
|
393
394
|
returns = self.tsdf.copy()
|
@@ -424,12 +425,27 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
424
425
|
An OpenFrame object
|
425
426
|
|
426
427
|
"""
|
428
|
+
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
429
|
+
if not any(vtypes):
|
430
|
+
value_type = ValueType.PRICE
|
431
|
+
elif all(vtypes):
|
432
|
+
value_type = ValueType.RTRN
|
433
|
+
else:
|
434
|
+
msg = "Mix of series types will give inconsistent results"
|
435
|
+
raise MixedValuetypesError(msg)
|
436
|
+
|
427
437
|
self.tsdf.index = DatetimeIndex(self.tsdf.index)
|
428
|
-
|
438
|
+
if value_type == ValueType.PRICE:
|
439
|
+
self.tsdf = self.tsdf.resample(freq).last()
|
440
|
+
else:
|
441
|
+
self.tsdf = self.tsdf.resample(freq).sum()
|
429
442
|
self.tsdf.index = Index(d.date() for d in DatetimeIndex(self.tsdf.index))
|
430
443
|
for xerie in self.constituents:
|
431
444
|
xerie.tsdf.index = DatetimeIndex(xerie.tsdf.index)
|
432
|
-
|
445
|
+
if value_type == ValueType.PRICE:
|
446
|
+
xerie.tsdf = xerie.tsdf.resample(freq).last()
|
447
|
+
else:
|
448
|
+
xerie.tsdf = xerie.tsdf.resample(freq).sum()
|
433
449
|
xerie.tsdf.index = Index(
|
434
450
|
dejt.date() for dejt in DatetimeIndex(xerie.tsdf.index)
|
435
451
|
)
|
@@ -458,6 +474,15 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
458
474
|
An OpenFrame object
|
459
475
|
|
460
476
|
"""
|
477
|
+
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
478
|
+
if any(vtypes):
|
479
|
+
msg = (
|
480
|
+
"Do not run resample_to_business_period_ends on return series. "
|
481
|
+
"The operation will pick the last data point in the sparser series. "
|
482
|
+
"It will not sum returns and therefore data will be lost."
|
483
|
+
)
|
484
|
+
raise ResampleDataLossError(msg)
|
485
|
+
|
461
486
|
for xerie in self.constituents:
|
462
487
|
dates = _do_resample_to_business_period_ends(
|
463
488
|
data=xerie.tsdf,
|
@@ -530,7 +555,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
530
555
|
Series volatilities and correlation
|
531
556
|
|
532
557
|
"""
|
533
|
-
earlier, later = self.calc_range(
|
558
|
+
earlier, later = self.calc_range(
|
559
|
+
months_offset=months_from_last, from_dt=from_date, to_dt=to_date
|
560
|
+
)
|
534
561
|
if periods_in_a_year_fixed is None:
|
535
562
|
fraction = (later - earlier).days / 365.25
|
536
563
|
how_many = (
|
@@ -621,9 +648,13 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
621
648
|
Correlation matrix
|
622
649
|
|
623
650
|
"""
|
624
|
-
corr_matrix =
|
625
|
-
|
626
|
-
|
651
|
+
corr_matrix = (
|
652
|
+
self.tsdf.ffill()
|
653
|
+
.pct_change()
|
654
|
+
.corr(
|
655
|
+
method="pearson",
|
656
|
+
min_periods=1,
|
657
|
+
)
|
627
658
|
)
|
628
659
|
corr_matrix.columns = corr_matrix.columns.droplevel(level=1)
|
629
660
|
corr_matrix.index = corr_matrix.index.droplevel(level=1)
|
@@ -805,7 +836,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
805
836
|
Tracking Errors
|
806
837
|
|
807
838
|
"""
|
808
|
-
earlier, later = self.calc_range(
|
839
|
+
earlier, later = self.calc_range(
|
840
|
+
months_offset=months_from_last, from_dt=from_date, to_dt=to_date
|
841
|
+
)
|
809
842
|
fraction = (later - earlier).days / 365.25
|
810
843
|
|
811
844
|
msg = "base_column should be a tuple[str, ValueType] or an integer."
|
@@ -848,10 +881,8 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
848
881
|
:,
|
849
882
|
item,
|
850
883
|
]
|
851
|
-
relative =
|
852
|
-
vol = float(
|
853
|
-
relative.pct_change().std() * sqrt(time_factor),
|
854
|
-
)
|
884
|
+
relative = longdf.ffill().pct_change() - shortdf.ffill().pct_change()
|
885
|
+
vol = float(relative.std() * sqrt(time_factor))
|
855
886
|
terrors.append(vol)
|
856
887
|
|
857
888
|
return Series(
|
@@ -897,7 +928,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
897
928
|
Information Ratios
|
898
929
|
|
899
930
|
"""
|
900
|
-
earlier, later = self.calc_range(
|
931
|
+
earlier, later = self.calc_range(
|
932
|
+
months_offset=months_from_last, from_dt=from_date, to_dt=to_date
|
933
|
+
)
|
901
934
|
fraction = (later - earlier).days / 365.25
|
902
935
|
|
903
936
|
msg = "base_column should be a tuple[str, ValueType] or an integer."
|
@@ -940,13 +973,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
940
973
|
:,
|
941
974
|
item,
|
942
975
|
]
|
943
|
-
relative =
|
944
|
-
ret = float(
|
945
|
-
|
946
|
-
)
|
947
|
-
vol = float(
|
948
|
-
relative.pct_change().std() * sqrt(time_factor),
|
949
|
-
)
|
976
|
+
relative = longdf.ffill().pct_change() - shortdf.ffill().pct_change()
|
977
|
+
ret = float(relative.mean() * time_factor)
|
978
|
+
vol = float(relative.std() * sqrt(time_factor))
|
950
979
|
ratios.append(ret / vol)
|
951
980
|
|
952
981
|
return Series(
|
@@ -1000,7 +1029,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1000
1029
|
|
1001
1030
|
"""
|
1002
1031
|
loss_limit: float = 0.0
|
1003
|
-
earlier, later = self.calc_range(
|
1032
|
+
earlier, later = self.calc_range(
|
1033
|
+
months_offset=months_from_last, from_dt=from_date, to_dt=to_date
|
1034
|
+
)
|
1004
1035
|
fraction = (later - earlier).days / 365.25
|
1005
1036
|
|
1006
1037
|
msg = "base_column should be a tuple[str, ValueType] or an integer."
|
@@ -1046,16 +1077,18 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1046
1077
|
msg = "ratio must be one of 'up', 'down' or 'both'."
|
1047
1078
|
if ratio == "up":
|
1048
1079
|
uparray = (
|
1049
|
-
longdf.
|
1050
|
-
|
1080
|
+
longdf.ffill()
|
1081
|
+
.pct_change()[
|
1082
|
+
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1051
1083
|
]
|
1052
1084
|
.add(1)
|
1053
1085
|
.to_numpy()
|
1054
1086
|
)
|
1055
1087
|
up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
|
1056
1088
|
upidxarray = (
|
1057
|
-
shortdf.
|
1058
|
-
|
1089
|
+
shortdf.ffill()
|
1090
|
+
.pct_change()[
|
1091
|
+
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1059
1092
|
]
|
1060
1093
|
.add(1)
|
1061
1094
|
.to_numpy()
|
@@ -1066,8 +1099,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1066
1099
|
ratios.append(up_rtrn / up_idx_return)
|
1067
1100
|
elif ratio == "down":
|
1068
1101
|
downarray = (
|
1069
|
-
longdf.
|
1070
|
-
|
1102
|
+
longdf.ffill()
|
1103
|
+
.pct_change()[
|
1104
|
+
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1071
1105
|
]
|
1072
1106
|
.add(1)
|
1073
1107
|
.to_numpy()
|
@@ -1076,8 +1110,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1076
1110
|
downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
|
1077
1111
|
)
|
1078
1112
|
downidxarray = (
|
1079
|
-
shortdf.
|
1080
|
-
|
1113
|
+
shortdf.ffill()
|
1114
|
+
.pct_change()[
|
1115
|
+
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1081
1116
|
]
|
1082
1117
|
.add(1)
|
1083
1118
|
.to_numpy()
|
@@ -1089,16 +1124,18 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1089
1124
|
ratios.append(down_return / down_idx_return)
|
1090
1125
|
elif ratio == "both":
|
1091
1126
|
uparray = (
|
1092
|
-
longdf.
|
1093
|
-
|
1127
|
+
longdf.ffill()
|
1128
|
+
.pct_change()[
|
1129
|
+
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1094
1130
|
]
|
1095
1131
|
.add(1)
|
1096
1132
|
.to_numpy()
|
1097
1133
|
)
|
1098
1134
|
up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
|
1099
1135
|
upidxarray = (
|
1100
|
-
shortdf.
|
1101
|
-
|
1136
|
+
shortdf.ffill()
|
1137
|
+
.pct_change()[
|
1138
|
+
shortdf.ffill().pct_change().to_numpy() > loss_limit
|
1102
1139
|
]
|
1103
1140
|
.add(1)
|
1104
1141
|
.to_numpy()
|
@@ -1107,8 +1144,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1107
1144
|
upidxarray.prod() ** (1 / (len(upidxarray) / time_factor)) - 1
|
1108
1145
|
)
|
1109
1146
|
downarray = (
|
1110
|
-
longdf.
|
1111
|
-
|
1147
|
+
longdf.ffill()
|
1148
|
+
.pct_change()[
|
1149
|
+
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1112
1150
|
]
|
1113
1151
|
.add(1)
|
1114
1152
|
.to_numpy()
|
@@ -1117,8 +1155,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1117
1155
|
downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
|
1118
1156
|
)
|
1119
1157
|
downidxarray = (
|
1120
|
-
shortdf.
|
1121
|
-
|
1158
|
+
shortdf.ffill()
|
1159
|
+
.pct_change()[
|
1160
|
+
shortdf.ffill().pct_change().to_numpy() < loss_limit
|
1122
1161
|
]
|
1123
1162
|
.add(1)
|
1124
1163
|
.to_numpy()
|
@@ -1173,10 +1212,8 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1173
1212
|
Beta as Co-variance of x & y divided by Variance of x
|
1174
1213
|
|
1175
1214
|
"""
|
1176
|
-
|
1177
|
-
|
1178
|
-
for x_value in self.tsdf.columns.get_level_values(1).to_numpy()
|
1179
|
-
):
|
1215
|
+
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
1216
|
+
if all(vtypes):
|
1180
1217
|
msg = "asset should be a tuple[str, ValueType] or an integer."
|
1181
1218
|
if isinstance(asset, tuple):
|
1182
1219
|
y_value = self.tsdf.loc[:, asset]
|
@@ -1192,33 +1229,27 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1192
1229
|
x_value = self.tsdf.iloc[:, market]
|
1193
1230
|
else:
|
1194
1231
|
raise TypeError(msg)
|
1195
|
-
|
1232
|
+
elif not any(vtypes):
|
1196
1233
|
msg = "asset should be a tuple[str, ValueType] or an integer."
|
1197
1234
|
if isinstance(asset, tuple):
|
1198
|
-
y_value =
|
1199
|
-
self.tsdf.loc[:, asset] / self.tsdf.loc[:, asset].iloc[0],
|
1200
|
-
)
|
1235
|
+
y_value = self.tsdf.loc[:, asset].ffill().pct_change().iloc[1:]
|
1201
1236
|
elif isinstance(asset, int):
|
1202
|
-
y_value =
|
1203
|
-
self.tsdf.iloc[:, asset] / cast("float", self.tsdf.iloc[0, asset]),
|
1204
|
-
)
|
1237
|
+
y_value = self.tsdf.iloc[:, asset].ffill().pct_change().iloc[1:]
|
1205
1238
|
else:
|
1206
1239
|
raise TypeError(msg)
|
1207
|
-
|
1208
1240
|
msg = "market should be a tuple[str, ValueType] or an integer."
|
1241
|
+
|
1209
1242
|
if isinstance(market, tuple):
|
1210
|
-
x_value =
|
1211
|
-
self.tsdf.loc[:, market] / self.tsdf.loc[:, market].iloc[0],
|
1212
|
-
)
|
1243
|
+
x_value = self.tsdf.loc[:, market].ffill().pct_change().iloc[1:]
|
1213
1244
|
elif isinstance(market, int):
|
1214
|
-
x_value =
|
1215
|
-
self.tsdf.iloc[:, market]
|
1216
|
-
/ cast("float", self.tsdf.iloc[0, market]),
|
1217
|
-
)
|
1245
|
+
x_value = self.tsdf.iloc[:, market].ffill().pct_change().iloc[1:]
|
1218
1246
|
else:
|
1219
1247
|
raise TypeError(msg)
|
1248
|
+
else:
|
1249
|
+
msg = "Mix of series types will give inconsistent results"
|
1250
|
+
raise MixedValuetypesError(msg)
|
1220
1251
|
|
1221
|
-
covariance = cov(y_value, x_value, ddof=dlta_degr_freedms)
|
1252
|
+
covariance = cov(m=y_value, y=x_value, ddof=dlta_degr_freedms)
|
1222
1253
|
beta = covariance[0, 1] / covariance[1, 1]
|
1223
1254
|
|
1224
1255
|
return float(beta)
|
@@ -1319,105 +1350,57 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1319
1350
|
Jensen's alpha
|
1320
1351
|
|
1321
1352
|
"""
|
1322
|
-
full_year = 1.0
|
1323
1353
|
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
1324
1354
|
if not any(vtypes):
|
1325
1355
|
msg = "asset should be a tuple[str, ValueType] or an integer."
|
1326
1356
|
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
|
-
)
|
1357
|
+
asset_rtn = self.tsdf.loc[:, asset].ffill().pct_change().iloc[1:]
|
1358
|
+
asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
|
1341
1359
|
elif isinstance(asset, int):
|
1342
|
-
|
1343
|
-
|
1344
|
-
)
|
1345
|
-
if self.yearfrac > full_year:
|
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
|
-
)
|
1360
|
+
asset_rtn = self.tsdf.iloc[:, asset].ffill().pct_change().iloc[1:]
|
1361
|
+
asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
|
1356
1362
|
else:
|
1357
1363
|
raise TypeError(msg)
|
1358
1364
|
|
1359
1365
|
msg = "market should be a tuple[str, ValueType] or an integer."
|
1360
1366
|
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
|
-
)
|
1367
|
+
market_rtn = self.tsdf.loc[:, market].ffill().pct_change().iloc[1:]
|
1368
|
+
market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
|
1375
1369
|
elif isinstance(market, int):
|
1376
|
-
|
1377
|
-
|
1378
|
-
/ cast("float", self.tsdf.iloc[0, market]),
|
1379
|
-
)
|
1380
|
-
if self.yearfrac > full_year:
|
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
|
-
)
|
1370
|
+
market_rtn = self.tsdf.iloc[:, market].ffill().pct_change().iloc[1:]
|
1371
|
+
market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
|
1391
1372
|
else:
|
1392
1373
|
raise TypeError(msg)
|
1393
1374
|
elif all(vtypes):
|
1394
1375
|
msg = "asset should be a tuple[str, ValueType] or an integer."
|
1395
1376
|
if isinstance(asset, tuple):
|
1396
|
-
|
1397
|
-
|
1377
|
+
asset_rtn = self.tsdf.loc[:, asset]
|
1378
|
+
asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
|
1398
1379
|
elif isinstance(asset, int):
|
1399
|
-
|
1400
|
-
|
1380
|
+
asset_rtn = self.tsdf.iloc[:, asset]
|
1381
|
+
asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
|
1401
1382
|
else:
|
1402
1383
|
raise TypeError(msg)
|
1403
1384
|
|
1404
1385
|
msg = "market should be a tuple[str, ValueType] or an integer."
|
1405
1386
|
if isinstance(market, tuple):
|
1406
|
-
|
1407
|
-
|
1387
|
+
market_rtn = self.tsdf.loc[:, market]
|
1388
|
+
market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
|
1408
1389
|
elif isinstance(market, int):
|
1409
|
-
|
1410
|
-
|
1390
|
+
market_rtn = self.tsdf.iloc[:, market]
|
1391
|
+
market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
|
1411
1392
|
else:
|
1412
1393
|
raise TypeError(msg)
|
1413
1394
|
else:
|
1414
1395
|
msg = "Mix of series types will give inconsistent results"
|
1415
1396
|
raise MixedValuetypesError(msg)
|
1416
1397
|
|
1417
|
-
covariance = cov(m=
|
1398
|
+
covariance = cov(m=asset_rtn, y=market_rtn, ddof=dlta_degr_freedms)
|
1418
1399
|
beta = covariance[0, 1] / covariance[1, 1]
|
1419
1400
|
|
1420
|
-
return float(
|
1401
|
+
return float(
|
1402
|
+
asset_rtn_mean - riskfree_rate - beta * (market_rtn_mean - riskfree_rate)
|
1403
|
+
)
|
1421
1404
|
|
1422
1405
|
def make_portfolio(
|
1423
1406
|
self: Self,
|
@@ -1448,7 +1431,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1448
1431
|
|
1449
1432
|
vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
|
1450
1433
|
if not any(vtypes):
|
1451
|
-
returns = self.tsdf.pct_change()
|
1434
|
+
returns = self.tsdf.ffill().pct_change()
|
1452
1435
|
returns.iloc[0] = 0
|
1453
1436
|
elif all(vtypes):
|
1454
1437
|
returns = self.tsdf.copy()
|
@@ -1523,11 +1506,14 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1523
1506
|
)
|
1524
1507
|
|
1525
1508
|
retseries = (
|
1526
|
-
relative.
|
1509
|
+
relative.ffill()
|
1510
|
+
.pct_change()
|
1511
|
+
.rolling(observations, min_periods=observations)
|
1512
|
+
.sum()
|
1527
1513
|
)
|
1528
1514
|
retdf = retseries.dropna().to_frame()
|
1529
1515
|
|
1530
|
-
voldf = relative.pct_change().rolling(
|
1516
|
+
voldf = relative.ffill().pct_change().rolling(
|
1531
1517
|
observations,
|
1532
1518
|
min_periods=observations,
|
1533
1519
|
).std() * sqrt(time_factor)
|
@@ -1573,9 +1559,13 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1573
1559
|
asset_label = cast("tuple[str, str]", self.tsdf.iloc[:, asset_column].name)[0]
|
1574
1560
|
beta_label = f"{asset_label} / {market_label}"
|
1575
1561
|
|
1576
|
-
rolling =
|
1577
|
-
|
1578
|
-
|
1562
|
+
rolling = (
|
1563
|
+
self.tsdf.ffill()
|
1564
|
+
.pct_change()
|
1565
|
+
.rolling(
|
1566
|
+
observations,
|
1567
|
+
min_periods=observations,
|
1568
|
+
)
|
1579
1569
|
)
|
1580
1570
|
|
1581
1571
|
rcov = rolling.cov(ddof=dlta_degr_freedms)
|
@@ -1630,10 +1620,11 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
|
|
1630
1620
|
)
|
1631
1621
|
first_series = (
|
1632
1622
|
self.tsdf.iloc[:, first_column]
|
1623
|
+
.ffill()
|
1633
1624
|
.pct_change()[1:]
|
1634
1625
|
.rolling(observations, min_periods=observations)
|
1635
1626
|
)
|
1636
|
-
second_series = self.tsdf.iloc[:, second_column].pct_change()[1:]
|
1627
|
+
second_series = self.tsdf.iloc[:, second_column].ffill().pct_change()[1:]
|
1637
1628
|
corrdf = first_series.corr(other=second_series).dropna().to_frame()
|
1638
1629
|
corrdf.columns = MultiIndex.from_arrays(
|
1639
1630
|
[
|
@@ -376,3 +376,7 @@ class IncorrectArgumentComboError(Exception):
|
|
376
376
|
|
377
377
|
class PropertiesInputValidationError(Exception):
|
378
378
|
"""Raised when duplicate strings are provided."""
|
379
|
+
|
380
|
+
|
381
|
+
class ResampleDataLossError(Exception):
|
382
|
+
"""Raised when user attempts to run resample_to_business_period_ends on returns."""
|
@@ -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.pct_change()
|
323
|
+
line_df["stdev_diff"] = line_df.stdev.ffill().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
|
|
@@ -68,9 +68,7 @@ def calendar_period_returns(
|
|
68
68
|
"""
|
69
69
|
copied = data.from_deepcopy()
|
70
70
|
copied.resample_to_business_period_ends(freq=freq)
|
71
|
-
|
72
|
-
if not any(vtypes):
|
73
|
-
copied.value_to_ret()
|
71
|
+
copied.value_to_ret()
|
74
72
|
cldr = copied.tsdf.iloc[1:].copy()
|
75
73
|
if relabel:
|
76
74
|
if freq.upper() == "BYE":
|
@@ -53,6 +53,7 @@ from .owntypes import (
|
|
53
53
|
LiteralSeriesProps,
|
54
54
|
MarketsNotStringNorListStrError,
|
55
55
|
OpenTimeSeriesPropertiesList,
|
56
|
+
ResampleDataLossError,
|
56
57
|
Self,
|
57
58
|
ValueListType,
|
58
59
|
ValueType,
|
@@ -66,7 +67,7 @@ TypeOpenTimeSeries = TypeVar("TypeOpenTimeSeries", bound="OpenTimeSeries")
|
|
66
67
|
|
67
68
|
|
68
69
|
# noinspection PyUnresolvedReferences,PyNestedDecorators
|
69
|
-
class OpenTimeSeries(_CommonModel):
|
70
|
+
class OpenTimeSeries(_CommonModel): # type: ignore[misc]
|
70
71
|
"""OpenTimeSeries objects are at the core of the openseries package.
|
71
72
|
|
72
73
|
The intended use is to allow analyses of financial timeseries.
|
@@ -468,7 +469,7 @@ class OpenTimeSeries(_CommonModel):
|
|
468
469
|
The returns of the values in the series
|
469
470
|
|
470
471
|
"""
|
471
|
-
returns = self.tsdf.pct_change()
|
472
|
+
returns = self.tsdf.ffill().pct_change()
|
472
473
|
returns.iloc[0] = 0
|
473
474
|
self.valuetype = ValueType.RTRN
|
474
475
|
arrays = [[self.label], [self.valuetype]]
|
@@ -586,7 +587,10 @@ class OpenTimeSeries(_CommonModel):
|
|
586
587
|
|
587
588
|
"""
|
588
589
|
self.tsdf.index = DatetimeIndex(self.tsdf.index)
|
589
|
-
self.
|
590
|
+
if self.valuetype == ValueType.RTRN:
|
591
|
+
self.tsdf = self.tsdf.resample(freq).sum()
|
592
|
+
else:
|
593
|
+
self.tsdf = self.tsdf.resample(freq).last()
|
590
594
|
self.tsdf.index = Index(d.date() for d in DatetimeIndex(self.tsdf.index))
|
591
595
|
return self
|
592
596
|
|
@@ -612,6 +616,14 @@ class OpenTimeSeries(_CommonModel):
|
|
612
616
|
An OpenTimeSeries object
|
613
617
|
|
614
618
|
"""
|
619
|
+
if self.valuetype == ValueType.RTRN:
|
620
|
+
msg = (
|
621
|
+
"Do not run resample_to_business_period_ends on return series. "
|
622
|
+
"The operation will pick the last data point in the sparser series. "
|
623
|
+
"It will not sum returns and therefore data will be lost."
|
624
|
+
)
|
625
|
+
raise ResampleDataLossError(msg)
|
626
|
+
|
615
627
|
dates = _do_resample_to_business_period_ends(
|
616
628
|
data=self.tsdf,
|
617
629
|
freq=freq,
|
@@ -659,7 +671,9 @@ class OpenTimeSeries(_CommonModel):
|
|
659
671
|
Series EWMA volatility
|
660
672
|
|
661
673
|
"""
|
662
|
-
earlier, later = self.calc_range(
|
674
|
+
earlier, later = self.calc_range(
|
675
|
+
months_offset=months_from_last, from_dt=from_date, to_dt=to_date
|
676
|
+
)
|
663
677
|
if periods_in_a_year_fixed:
|
664
678
|
time_factor = float(periods_in_a_year_fixed)
|
665
679
|
else:
|
@@ -725,7 +739,7 @@ class OpenTimeSeries(_CommonModel):
|
|
725
739
|
returns_input = True
|
726
740
|
else:
|
727
741
|
values = [cast("float", self.tsdf.iloc[0, 0])]
|
728
|
-
ra_df = self.tsdf.pct_change()
|
742
|
+
ra_df = self.tsdf.ffill().pct_change()
|
729
743
|
returns_input = False
|
730
744
|
ra_df = ra_df.dropna()
|
731
745
|
|
@@ -129,7 +129,9 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
|
|
129
129
|
"""
|
130
130
|
return cast(
|
131
131
|
"float",
|
132
|
-
(
|
132
|
+
(
|
133
|
+
self.results.ffill().pct_change().mean() * self.trading_days_in_year
|
134
|
+
).iloc[0],
|
133
135
|
)
|
134
136
|
|
135
137
|
@property
|
@@ -144,9 +146,10 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
|
|
144
146
|
"""
|
145
147
|
return cast(
|
146
148
|
"float",
|
147
|
-
(
|
148
|
-
|
149
|
-
|
149
|
+
(
|
150
|
+
self.results.ffill().pct_change().std()
|
151
|
+
* sqrt(self.trading_days_in_year)
|
152
|
+
).iloc[0],
|
150
153
|
)
|
151
154
|
|
152
155
|
@classmethod
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "openseries"
|
3
|
-
version = "1.9.
|
3
|
+
version = "1.9.4"
|
4
4
|
description = "Tools for analyzing financial timeseries."
|
5
5
|
authors = [
|
6
6
|
{ name = "Martin Karrin", email = "martin.karrin@captor.se" },
|
@@ -67,7 +67,7 @@ pre-commit = ">=3.7.1,<6.0.0"
|
|
67
67
|
pytest = ">=8.2.2,<9.0.0"
|
68
68
|
pytest-cov = ">=5.0.0,<7.0.0"
|
69
69
|
pytest-xdist = ">=3.3.1,<5.0.0"
|
70
|
-
ruff = "0.12.
|
70
|
+
ruff = "0.12.10"
|
71
71
|
types-openpyxl = ">=3.1.2,<5.0.0"
|
72
72
|
types-python-dateutil = ">=2.8.2,<4.0.0"
|
73
73
|
types-requests = ">=2.20.0,<3.0.0"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|