openseries 1.9.2__py3-none-any.whl → 1.9.4__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 +96 -28
- openseries/frame.py +188 -142
- openseries/load_plotly.py +1 -1
- openseries/owntypes.py +7 -0
- openseries/portfoliotools.py +10 -6
- openseries/report.py +4 -6
- openseries/series.py +21 -7
- openseries/simulation.py +2 -3
- {openseries-1.9.2.dist-info → openseries-1.9.4.dist-info}/METADATA +64 -62
- openseries-1.9.4.dist-info/RECORD +17 -0
- openseries-1.9.2.dist-info/RECORD +0 -17
- {openseries-1.9.2.dist-info → openseries-1.9.4.dist-info}/LICENSE.md +0 -0
- {openseries-1.9.2.dist-info → openseries-1.9.4.dist-info}/WHEEL +0 -0
openseries/_common_model.py
CHANGED
@@ -17,7 +17,7 @@ from math import ceil
|
|
17
17
|
from pathlib import Path
|
18
18
|
from secrets import choice
|
19
19
|
from string import ascii_letters
|
20
|
-
from typing import TYPE_CHECKING, Any, SupportsFloat, cast
|
20
|
+
from typing import TYPE_CHECKING, Any, Literal, SupportsFloat, cast
|
21
21
|
|
22
22
|
from numpy import float64, inf, isnan, log, maximum, sqrt
|
23
23
|
|
@@ -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
|
@@ -263,7 +264,10 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
263
264
|
|
264
265
|
"""
|
265
266
|
min_accepted_return: float = 0.0
|
266
|
-
|
267
|
+
order: Literal[2, 3] = 2
|
268
|
+
return self.lower_partial_moment_func(
|
269
|
+
min_accepted_return=min_accepted_return, order=order
|
270
|
+
)
|
267
271
|
|
268
272
|
@property
|
269
273
|
def ret_vol_ratio(self: Self) -> float | Series[float]:
|
@@ -298,6 +302,32 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
298
302
|
min_accepted_return=minimum_accepted_return,
|
299
303
|
)
|
300
304
|
|
305
|
+
@property
|
306
|
+
def kappa3_ratio(self: Self) -> float | Series[float]:
|
307
|
+
"""Kappa-3 ratio.
|
308
|
+
|
309
|
+
The Kappa-3 ratio is a generalized downside-risk ratio defined as
|
310
|
+
annualized arithmetic return divided by the cubic-root of the
|
311
|
+
lower partial moment of order 3 (with respect to a minimum acceptable
|
312
|
+
return, MAR). It penalizes larger downside outcomes more heavily than
|
313
|
+
the Sortino ratio (which uses order 2).
|
314
|
+
|
315
|
+
Returns:
|
316
|
+
-------
|
317
|
+
float | Pandas.Series[float]
|
318
|
+
Kappa-3 ratio calculation with the riskfree rate and
|
319
|
+
Minimum Acceptable Return (MAR) both set to zero.
|
320
|
+
|
321
|
+
"""
|
322
|
+
riskfree_rate: float = 0.0
|
323
|
+
minimum_accepted_return: float = 0.0
|
324
|
+
order: Literal[2, 3] = 3
|
325
|
+
return self.sortino_ratio_func(
|
326
|
+
riskfree_rate=riskfree_rate,
|
327
|
+
min_accepted_return=minimum_accepted_return,
|
328
|
+
order=order,
|
329
|
+
)
|
330
|
+
|
301
331
|
@property
|
302
332
|
def omega_ratio(self: Self) -> float | Series[float]:
|
303
333
|
"""https://en.wikipedia.org/wiki/Omega_ratio.
|
@@ -403,6 +433,16 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
403
433
|
|
404
434
|
wmdf = wmdf.reindex(index=[deyt.date() for deyt in dates], method=method)
|
405
435
|
wmdf.index = DatetimeIndex(wmdf.index)
|
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
|
+
|
406
446
|
result = wmdf.ffill().pct_change().min()
|
407
447
|
|
408
448
|
if self.tsdf.shape[1] == 1:
|
@@ -559,6 +599,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
559
599
|
countries: CountriesType | None = None,
|
560
600
|
markets: list[str] | str | None = None,
|
561
601
|
custom_holidays: list[str] | str | None = None,
|
602
|
+
method: LiteralPandasReindexMethod = "nearest",
|
562
603
|
) -> Self:
|
563
604
|
"""Align the index of .tsdf with local calendar business days.
|
564
605
|
|
@@ -570,6 +611,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
570
611
|
(List of) markets code(s) according to pandas-market-calendars
|
571
612
|
custom_holidays: list[str] | str, optional
|
572
613
|
Argument where missing holidays can be added
|
614
|
+
method: LiteralPandasReindexMethod, default: "nearest"
|
573
615
|
|
574
616
|
Returns:
|
575
617
|
-------
|
@@ -620,7 +662,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
620
662
|
freq=CustomBusinessDay(calendar=calendar),
|
621
663
|
)
|
622
664
|
]
|
623
|
-
self.tsdf = self.tsdf.reindex(d_range, method=
|
665
|
+
self.tsdf = self.tsdf.reindex(labels=d_range, method=method, copy=False)
|
624
666
|
|
625
667
|
return self
|
626
668
|
|
@@ -1016,7 +1058,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
1016
1058
|
)
|
1017
1059
|
figure.update_layout(yaxis={"tickformat": tick_fmt})
|
1018
1060
|
|
1019
|
-
if show_last
|
1061
|
+
if show_last:
|
1020
1062
|
txt = f"Last {{:{tick_fmt}}}" if tick_fmt else "Last {}"
|
1021
1063
|
|
1022
1064
|
for item in range(self.tsdf.shape[1]):
|
@@ -1063,7 +1105,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
1063
1105
|
def plot_histogram(
|
1064
1106
|
self: Self,
|
1065
1107
|
plot_type: LiteralPlotlyHistogramPlotType = "bars",
|
1066
|
-
histnorm: LiteralPlotlyHistogramHistNorm = "
|
1108
|
+
histnorm: LiteralPlotlyHistogramHistNorm = "probability",
|
1067
1109
|
barmode: LiteralPlotlyHistogramBarMode = "overlay",
|
1068
1110
|
xbins_size: float | None = None,
|
1069
1111
|
opacity: float = 0.75,
|
@@ -1167,6 +1209,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
1167
1209
|
figure.add_histogram(
|
1168
1210
|
x=self.tsdf.iloc[:, item],
|
1169
1211
|
cumulative={"enabled": cumulative},
|
1212
|
+
histfunc="count",
|
1170
1213
|
histnorm=histnorm,
|
1171
1214
|
name=labels[item],
|
1172
1215
|
xbins={"size": xbins_size},
|
@@ -1640,24 +1683,28 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
1640
1683
|
dtype="float64",
|
1641
1684
|
)
|
1642
1685
|
|
1643
|
-
def
|
1686
|
+
def lower_partial_moment_func(
|
1644
1687
|
self: Self,
|
1645
1688
|
min_accepted_return: float = 0.0,
|
1689
|
+
order: Literal[2, 3] = 2,
|
1646
1690
|
months_from_last: int | None = None,
|
1647
1691
|
from_date: dt.date | None = None,
|
1648
1692
|
to_date: dt.date | None = None,
|
1649
1693
|
periods_in_a_year_fixed: DaysInYearType | None = None,
|
1650
1694
|
) -> float | Series[float]:
|
1651
|
-
"""Downside Deviation.
|
1695
|
+
"""Downside Deviation if order set to 2.
|
1652
1696
|
|
1653
|
-
|
1654
|
-
|
1655
|
-
|
1697
|
+
If order is set to 2 the function calculates the standard
|
1698
|
+
deviation of returns that are below a Minimum Accepted
|
1699
|
+
Return of zero. For general order p, it returns LPM_p^(1/p),
|
1700
|
+
i.e., the rooted lower partial moment of order p.
|
1656
1701
|
|
1657
1702
|
Parameters
|
1658
1703
|
----------
|
1659
1704
|
min_accepted_return : float, optional
|
1660
1705
|
The annualized Minimum Accepted Return (MAR)
|
1706
|
+
order: int, default: 2
|
1707
|
+
Order of partial moment
|
1661
1708
|
months_from_last : int, optional
|
1662
1709
|
number of months offset as positive integer. Overrides use of from_date
|
1663
1710
|
and to_date
|
@@ -1672,15 +1719,20 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
1672
1719
|
Returns:
|
1673
1720
|
-------
|
1674
1721
|
float | Pandas.Series[float]
|
1675
|
-
Downside deviation
|
1722
|
+
Downside deviation if order set to 2
|
1676
1723
|
|
1677
1724
|
"""
|
1725
|
+
msg = f"'order' must be 2 or 3, got {order!r}."
|
1726
|
+
if order not in (2, 3):
|
1727
|
+
raise ValueError(msg)
|
1728
|
+
|
1678
1729
|
zero: float = 0.0
|
1679
1730
|
earlier, later = self.calc_range(
|
1680
1731
|
months_offset=months_from_last,
|
1681
1732
|
from_dt=from_date,
|
1682
1733
|
to_dt=to_date,
|
1683
1734
|
)
|
1735
|
+
|
1684
1736
|
how_many = (
|
1685
1737
|
self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
1686
1738
|
.ffill()
|
@@ -1697,23 +1749,27 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
1697
1749
|
fraction = (later - earlier).days / 365.25
|
1698
1750
|
time_factor = how_many.div(fraction)
|
1699
1751
|
|
1700
|
-
|
1752
|
+
per_period_mar = min_accepted_return / time_factor
|
1753
|
+
diff = (
|
1701
1754
|
self.tsdf.loc[cast("int", earlier) : cast("int", later)]
|
1702
1755
|
.ffill()
|
1703
1756
|
.pct_change()
|
1704
|
-
.sub(
|
1757
|
+
.sub(per_period_mar)
|
1705
1758
|
)
|
1706
1759
|
|
1707
|
-
|
1708
|
-
|
1709
|
-
)
|
1760
|
+
shortfall = (-diff).clip(lower=zero)
|
1761
|
+
base = shortfall.pow(order).sum() / how_many
|
1762
|
+
result = base.pow(1.0 / float(order))
|
1763
|
+
result *= sqrt(time_factor)
|
1764
|
+
|
1765
|
+
dd_order = 2
|
1710
1766
|
|
1711
1767
|
if self.tsdf.shape[1] == 1:
|
1712
1768
|
return float(result.iloc[0])
|
1713
1769
|
return Series(
|
1714
1770
|
data=result,
|
1715
1771
|
index=self.tsdf.columns,
|
1716
|
-
name="Downside deviation",
|
1772
|
+
name="Downside deviation" if order == dd_order else f"LPM{order}",
|
1717
1773
|
dtype="float64",
|
1718
1774
|
)
|
1719
1775
|
|
@@ -2047,18 +2103,22 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
2047
2103
|
self: Self,
|
2048
2104
|
riskfree_rate: float = 0.0,
|
2049
2105
|
min_accepted_return: float = 0.0,
|
2106
|
+
order: Literal[2, 3] = 2,
|
2050
2107
|
months_from_last: int | None = None,
|
2051
2108
|
from_date: dt.date | None = None,
|
2052
2109
|
to_date: dt.date | None = None,
|
2053
2110
|
periods_in_a_year_fixed: DaysInYearType | None = None,
|
2054
2111
|
) -> float | Series[float]:
|
2055
|
-
"""Sortino Ratio.
|
2112
|
+
"""Sortino Ratio or Kappa3 Ratio.
|
2056
2113
|
|
2057
2114
|
The Sortino ratio calculated as ( return - risk free return )
|
2058
2115
|
/ downside deviation. The ratio implies that the riskfree asset has zero
|
2059
2116
|
volatility, and a minimum acceptable return of zero. The ratio is
|
2060
2117
|
calculated using the annualized arithmetic mean of returns.
|
2061
2118
|
https://www.investopedia.com/terms/s/sortinoratio.asp.
|
2119
|
+
If order is set to 3 the ratio calculated becomes Kappa3 which
|
2120
|
+
penalizes larger downside outcomes more heavily than the Sortino
|
2121
|
+
ratio (which uses order 2).
|
2062
2122
|
|
2063
2123
|
Parameters
|
2064
2124
|
----------
|
@@ -2066,6 +2126,8 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
2066
2126
|
The return of the zero volatility asset
|
2067
2127
|
min_accepted_return : float, optional
|
2068
2128
|
The annualized Minimum Accepted Return (MAR)
|
2129
|
+
order: int, default: 2
|
2130
|
+
Order of partial moment
|
2069
2131
|
months_from_last : int, optional
|
2070
2132
|
number of months offset as positive integer. Overrides use of from_date
|
2071
2133
|
and to_date
|
@@ -2092,20 +2154,22 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
2092
2154
|
periods_in_a_year_fixed=periods_in_a_year_fixed,
|
2093
2155
|
)
|
2094
2156
|
- riskfree_rate,
|
2095
|
-
) / self.
|
2157
|
+
) / self.lower_partial_moment_func(
|
2096
2158
|
min_accepted_return=min_accepted_return,
|
2159
|
+
order=order,
|
2097
2160
|
months_from_last=months_from_last,
|
2098
2161
|
from_date=from_date,
|
2099
2162
|
to_date=to_date,
|
2100
2163
|
periods_in_a_year_fixed=periods_in_a_year_fixed,
|
2101
2164
|
)
|
2102
2165
|
|
2166
|
+
sortino_order = 2
|
2103
2167
|
if self.tsdf.shape[1] == 1:
|
2104
2168
|
return float(cast("float64", ratio.iloc[0]))
|
2105
2169
|
return Series(
|
2106
2170
|
data=ratio,
|
2107
2171
|
index=self.tsdf.columns,
|
2108
|
-
name="Sortino ratio",
|
2172
|
+
name="Sortino ratio" if order == sortino_order else "Kappa-3 ratio",
|
2109
2173
|
dtype="float64",
|
2110
2174
|
)
|
2111
2175
|
|
@@ -2513,6 +2577,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
2513
2577
|
column: int = 0,
|
2514
2578
|
observations: int = 21,
|
2515
2579
|
periods_in_a_year_fixed: DaysInYearType | None = None,
|
2580
|
+
dlta_degr_freedms: int = 1,
|
2516
2581
|
) -> DataFrame:
|
2517
2582
|
"""Calculate rolling annualised volatilities.
|
2518
2583
|
|
@@ -2525,6 +2590,8 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
2525
2590
|
periods_in_a_year_fixed : DaysInYearType, optional
|
2526
2591
|
Allows locking the periods-in-a-year to simplify test cases and
|
2527
2592
|
comparisons
|
2593
|
+
dlta_degr_freedms: int, default: 1
|
2594
|
+
Variance bias factor taking the value 0 or 1.
|
2528
2595
|
|
2529
2596
|
Returns:
|
2530
2597
|
-------
|
@@ -2536,15 +2603,16 @@ class _CommonModel(BaseModel): # type: ignore[misc]
|
|
2536
2603
|
time_factor = float(periods_in_a_year_fixed)
|
2537
2604
|
else:
|
2538
2605
|
time_factor = self.periods_in_a_year
|
2606
|
+
|
2539
2607
|
vol_label = cast("tuple[str, ValueType]", self.tsdf.iloc[:, column].name)[0]
|
2540
|
-
|
2541
|
-
|
2542
|
-
|
2543
|
-
|
2544
|
-
)
|
2545
|
-
|
2546
|
-
)
|
2608
|
+
|
2609
|
+
s = log(self.tsdf.iloc[:, column]).diff()
|
2610
|
+
volseries = s.rolling(window=observations, min_periods=observations).std(
|
2611
|
+
ddof=dlta_degr_freedms
|
2612
|
+
) * sqrt(time_factor)
|
2613
|
+
|
2547
2614
|
voldf = volseries.dropna().to_frame()
|
2615
|
+
|
2548
2616
|
voldf.columns = MultiIndex.from_arrays(
|
2549
2617
|
[
|
2550
2618
|
[vol_label],
|