openseries 1.4.10__tar.gz → 1.4.12__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.4.10 → openseries-1.4.12}/LICENSE.md +1 -1
- {openseries-1.4.10 → openseries-1.4.12}/PKG-INFO +1 -1
- {openseries-1.4.10 → openseries-1.4.12}/openseries/_common_model.py +11 -14
- {openseries-1.4.10 → openseries-1.4.12}/openseries/_risk.py +2 -2
- {openseries-1.4.10 → openseries-1.4.12}/openseries/frame.py +31 -24
- {openseries-1.4.10 → openseries-1.4.12}/openseries/simulation.py +12 -0
- {openseries-1.4.10 → openseries-1.4.12}/pyproject.toml +11 -9
- {openseries-1.4.10 → openseries-1.4.12}/README.md +0 -0
- {openseries-1.4.10 → openseries-1.4.12}/openseries/__init__.py +0 -0
- {openseries-1.4.10 → openseries-1.4.12}/openseries/datefixer.py +0 -0
- {openseries-1.4.10 → openseries-1.4.12}/openseries/load_plotly.py +0 -0
- {openseries-1.4.10 → openseries-1.4.12}/openseries/plotly_captor_logo.json +0 -0
- {openseries-1.4.10 → openseries-1.4.12}/openseries/plotly_layouts.json +0 -0
- {openseries-1.4.10 → openseries-1.4.12}/openseries/series.py +0 -0
- {openseries-1.4.10 → openseries-1.4.12}/openseries/types.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
# BSD 3-Clause License
|
2
2
|
|
3
|
-
## Copyright (c)
|
3
|
+
## Copyright (c) 2024, Captor Fund Management AB
|
4
4
|
|
5
5
|
Redistribution and use in source and binary forms, with or without modification, are
|
6
6
|
permitted provided that the following conditions are met:
|
@@ -1,4 +1,5 @@
|
|
1
1
|
"""Defining the _CommonModel class."""
|
2
|
+
|
2
3
|
from __future__ import annotations
|
3
4
|
|
4
5
|
import datetime as dt
|
@@ -10,7 +11,7 @@ from secrets import choice
|
|
10
11
|
from string import ascii_letters
|
11
12
|
from typing import Any, Optional, Union, cast
|
12
13
|
|
13
|
-
from numpy import
|
14
|
+
from numpy import float64, inf, isnan, log, maximum, sqrt
|
14
15
|
from openpyxl.utils.dataframe import dataframe_to_rows
|
15
16
|
from openpyxl.workbook.workbook import Workbook
|
16
17
|
from openpyxl.worksheet.worksheet import Worksheet
|
@@ -484,7 +485,7 @@ class _CommonModel(BaseModel):
|
|
484
485
|
if any([months_offset, from_dt, to_dt]):
|
485
486
|
if months_offset is not None:
|
486
487
|
earlier = date_offset_foll(
|
487
|
-
raw_date=self.tsdf.index[-1],
|
488
|
+
raw_date=DatetimeIndex(self.tsdf.index)[-1],
|
488
489
|
months_offset=-months_offset,
|
489
490
|
adjust=False,
|
490
491
|
following=True,
|
@@ -537,8 +538,8 @@ class _CommonModel(BaseModel):
|
|
537
538
|
An OpenFrame object
|
538
539
|
|
539
540
|
"""
|
540
|
-
startyear = self.tsdf.index[0].year
|
541
|
-
endyear = self.tsdf.index[-1].year
|
541
|
+
startyear = DatetimeIndex(self.tsdf.index)[0].year
|
542
|
+
endyear = DatetimeIndex(self.tsdf.index)[-1].year
|
542
543
|
calendar = holiday_calendar(
|
543
544
|
startyear=startyear,
|
544
545
|
endyear=endyear,
|
@@ -629,7 +630,7 @@ class _CommonModel(BaseModel):
|
|
629
630
|
|
630
631
|
"""
|
631
632
|
drawdown = self.tsdf.copy()
|
632
|
-
drawdown[isnan(drawdown)] = -
|
633
|
+
drawdown[isnan(drawdown)] = -inf
|
633
634
|
roll_max = maximum.accumulate(drawdown, axis=0)
|
634
635
|
self.tsdf = DataFrame(drawdown / roll_max - 1.0)
|
635
636
|
return self
|
@@ -1514,24 +1515,20 @@ class _CommonModel(BaseModel):
|
|
1514
1515
|
)
|
1515
1516
|
fraction = (later - earlier).days / 365.25
|
1516
1517
|
|
1517
|
-
|
1518
|
-
|
1519
|
-
or self.tsdf.loc[[earlier, later]].lt(0.0).any().any()
|
1520
|
-
):
|
1518
|
+
any_below_zero = any(self.tsdf.loc[[earlier, later]].lt(0.0).any().to_numpy())
|
1519
|
+
if zero in self.tsdf.loc[earlier].to_numpy() or any_below_zero:
|
1521
1520
|
msg = (
|
1522
1521
|
"Geometric return cannot be calculated due to "
|
1523
1522
|
"an initial value being zero or a negative value."
|
1524
1523
|
)
|
1525
|
-
raise ValueError(
|
1526
|
-
msg,
|
1527
|
-
)
|
1524
|
+
raise ValueError(msg)
|
1528
1525
|
|
1529
1526
|
result = (self.tsdf.loc[later] / self.tsdf.loc[earlier]) ** (1 / fraction) - 1
|
1530
1527
|
|
1531
1528
|
if self.tsdf.shape[1] == 1:
|
1532
1529
|
return float(result.iloc[0])
|
1533
1530
|
return Series(
|
1534
|
-
data=result,
|
1531
|
+
data=result.to_numpy(),
|
1535
1532
|
index=self.tsdf.columns,
|
1536
1533
|
name="Geometric return",
|
1537
1534
|
dtype="float64",
|
@@ -1917,7 +1914,7 @@ class _CommonModel(BaseModel):
|
|
1917
1914
|
if self.tsdf.shape[1] == 1:
|
1918
1915
|
return float(result.iloc[0])
|
1919
1916
|
return Series(
|
1920
|
-
data=result,
|
1917
|
+
data=result.to_numpy(),
|
1921
1918
|
index=self.tsdf.columns,
|
1922
1919
|
name="Simple return",
|
1923
1920
|
dtype="float64",
|
@@ -5,11 +5,11 @@ from math import ceil
|
|
5
5
|
from typing import Union, cast
|
6
6
|
|
7
7
|
from numpy import (
|
8
|
-
NaN,
|
9
8
|
divide,
|
10
9
|
float64,
|
11
10
|
isinf,
|
12
11
|
mean,
|
12
|
+
nan,
|
13
13
|
nan_to_num,
|
14
14
|
quantile,
|
15
15
|
sort,
|
@@ -139,6 +139,6 @@ def _calc_inv_vol_weights(returns: DataFrame) -> NDArray[float64]:
|
|
139
139
|
|
140
140
|
"""
|
141
141
|
vol = divide(1.0, std(returns, axis=0, ddof=1))
|
142
|
-
vol[isinf(vol)] =
|
142
|
+
vol[isinf(vol)] = nan
|
143
143
|
volsum = vol.sum()
|
144
144
|
return cast(NDArray[float64], divide(vol, volsum))
|
@@ -168,6 +168,7 @@ class OpenFrame(_CommonModel):
|
|
168
168
|
An OpenFrame object
|
169
169
|
|
170
170
|
"""
|
171
|
+
lvl_zero = list(self.columns_lvl_zero)
|
171
172
|
self.tsdf = reduce(
|
172
173
|
lambda left, right: merge(
|
173
174
|
left=left,
|
@@ -178,14 +179,17 @@ class OpenFrame(_CommonModel):
|
|
178
179
|
),
|
179
180
|
[x.tsdf for x in self.constituents],
|
180
181
|
)
|
182
|
+
|
183
|
+
mapper = dict(zip(self.columns_lvl_zero, lvl_zero))
|
184
|
+
self.tsdf = self.tsdf.rename(columns=mapper, level=0)
|
185
|
+
|
181
186
|
if self.tsdf.empty:
|
182
187
|
msg = (
|
183
188
|
"Merging OpenTimeSeries DataFrames with "
|
184
189
|
f"argument how={how} produced an empty DataFrame."
|
185
190
|
)
|
186
|
-
raise ValueError(
|
187
|
-
|
188
|
-
)
|
191
|
+
raise ValueError(msg)
|
192
|
+
|
189
193
|
if how == "inner":
|
190
194
|
for xerie in self.constituents:
|
191
195
|
xerie.tsdf = xerie.tsdf.loc[self.tsdf.index]
|
@@ -454,8 +458,8 @@ class OpenFrame(_CommonModel):
|
|
454
458
|
tail = self.tsdf.loc[self.last_indices.min()].copy()
|
455
459
|
dates = do_resample_to_business_period_ends(
|
456
460
|
data=self.tsdf,
|
457
|
-
head=head,
|
458
|
-
tail=tail,
|
461
|
+
head=head, # type: ignore[arg-type]
|
462
|
+
tail=tail, # type: ignore[arg-type]
|
459
463
|
freq=freq,
|
460
464
|
countries=countries,
|
461
465
|
)
|
@@ -1046,9 +1050,7 @@ class OpenFrame(_CommonModel):
|
|
1046
1050
|
.add(1)
|
1047
1051
|
.to_numpy()
|
1048
1052
|
)
|
1049
|
-
|
1050
|
-
uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
|
1051
|
-
)
|
1053
|
+
up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
|
1052
1054
|
upidxarray = (
|
1053
1055
|
shortdf.pct_change(fill_method=cast(str, None))[
|
1054
1056
|
shortdf.pct_change(fill_method=cast(str, None)).to_numpy()
|
@@ -1060,7 +1062,7 @@ class OpenFrame(_CommonModel):
|
|
1060
1062
|
up_idx_return = (
|
1061
1063
|
upidxarray.prod() ** (1 / (len(upidxarray) / time_factor)) - 1
|
1062
1064
|
)
|
1063
|
-
ratios.append(
|
1065
|
+
ratios.append(up_rtrn / up_idx_return)
|
1064
1066
|
elif ratio == "down":
|
1065
1067
|
downarray = (
|
1066
1068
|
longdf.pct_change(fill_method=cast(str, None))[
|
@@ -1095,9 +1097,7 @@ class OpenFrame(_CommonModel):
|
|
1095
1097
|
.add(1)
|
1096
1098
|
.to_numpy()
|
1097
1099
|
)
|
1098
|
-
|
1099
|
-
uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
|
1100
|
-
)
|
1100
|
+
up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
|
1101
1101
|
upidxarray = (
|
1102
1102
|
shortdf.pct_change(fill_method=cast(str, None))[
|
1103
1103
|
shortdf.pct_change(fill_method=cast(str, None)).to_numpy()
|
@@ -1133,7 +1133,7 @@ class OpenFrame(_CommonModel):
|
|
1133
1133
|
- 1
|
1134
1134
|
)
|
1135
1135
|
ratios.append(
|
1136
|
-
(
|
1136
|
+
(up_rtrn / up_idx_return) / (down_return / down_idx_return),
|
1137
1137
|
)
|
1138
1138
|
|
1139
1139
|
if ratio == "up":
|
@@ -1154,6 +1154,7 @@ class OpenFrame(_CommonModel):
|
|
1154
1154
|
self: Self,
|
1155
1155
|
asset: Union[tuple[str, ValueType], int],
|
1156
1156
|
market: Union[tuple[str, ValueType], int],
|
1157
|
+
dlta_degr_freedms: int = 1,
|
1157
1158
|
) -> float:
|
1158
1159
|
"""
|
1159
1160
|
Market Beta.
|
@@ -1167,6 +1168,8 @@ class OpenFrame(_CommonModel):
|
|
1167
1168
|
The column of the asset
|
1168
1169
|
market: Union[tuple[str, ValueType], int]
|
1169
1170
|
The column of the market against which Beta is measured
|
1171
|
+
dlta_degr_freedms: int, default: 1
|
1172
|
+
Variance bias factor taking the value 0 or 1.
|
1170
1173
|
|
1171
1174
|
Returns
|
1172
1175
|
-------
|
@@ -1224,7 +1227,7 @@ class OpenFrame(_CommonModel):
|
|
1224
1227
|
msg,
|
1225
1228
|
)
|
1226
1229
|
|
1227
|
-
covariance = cov(y_value, x_value, ddof=
|
1230
|
+
covariance = cov(y_value, x_value, ddof=dlta_degr_freedms)
|
1228
1231
|
beta = covariance[0, 1] / covariance[1, 1]
|
1229
1232
|
|
1230
1233
|
return float(beta)
|
@@ -1305,6 +1308,7 @@ class OpenFrame(_CommonModel):
|
|
1305
1308
|
asset: Union[tuple[str, ValueType], int],
|
1306
1309
|
market: Union[tuple[str, ValueType], int],
|
1307
1310
|
riskfree_rate: float = 0.0,
|
1311
|
+
dlta_degr_freedms: int = 1,
|
1308
1312
|
) -> float:
|
1309
1313
|
"""
|
1310
1314
|
Jensen's alpha.
|
@@ -1324,6 +1328,8 @@ class OpenFrame(_CommonModel):
|
|
1324
1328
|
The column of the market against which Jensen's alpha is measured
|
1325
1329
|
riskfree_rate : float, default: 0.0
|
1326
1330
|
The return of the zero volatility riskfree asset
|
1331
|
+
dlta_degr_freedms: int, default: 1
|
1332
|
+
Variance bias factor taking the value 0 or 1.
|
1327
1333
|
|
1328
1334
|
Returns
|
1329
1335
|
-------
|
@@ -1430,7 +1436,7 @@ class OpenFrame(_CommonModel):
|
|
1430
1436
|
msg,
|
1431
1437
|
)
|
1432
1438
|
|
1433
|
-
covariance = cov(asset_log, market_log, ddof=
|
1439
|
+
covariance = cov(asset_log, market_log, ddof=dlta_degr_freedms)
|
1434
1440
|
beta = covariance[0, 1] / covariance[1, 1]
|
1435
1441
|
|
1436
1442
|
return float(asset_cagr - riskfree_rate - beta * (market_cagr - riskfree_rate))
|
@@ -1609,6 +1615,7 @@ class OpenFrame(_CommonModel):
|
|
1609
1615
|
asset_column: int = 0,
|
1610
1616
|
market_column: int = 1,
|
1611
1617
|
observations: int = 21,
|
1618
|
+
dlta_degr_freedms: int = 1,
|
1612
1619
|
) -> DataFrame:
|
1613
1620
|
"""
|
1614
1621
|
Calculate rolling Market Beta.
|
@@ -1624,6 +1631,8 @@ class OpenFrame(_CommonModel):
|
|
1624
1631
|
Column of timeseries that is the market.
|
1625
1632
|
observations: int, default: 21
|
1626
1633
|
The length of the rolling window to use is set as number of observations.
|
1634
|
+
dlta_degr_freedms: int, default: 1
|
1635
|
+
Variance bias factor taking the value 0 or 1.
|
1627
1636
|
|
1628
1637
|
Returns
|
1629
1638
|
-------
|
@@ -1635,13 +1644,13 @@ class OpenFrame(_CommonModel):
|
|
1635
1644
|
asset_label = cast(tuple[str, str], self.tsdf.iloc[:, asset_column].name)[0]
|
1636
1645
|
beta_label = f"{asset_label} / {market_label}"
|
1637
1646
|
|
1638
|
-
rolling = self.tsdf.copy()
|
1647
|
+
rolling: DataFrame = self.tsdf.copy()
|
1639
1648
|
rolling = rolling.pct_change(fill_method=cast(str, None)).rolling(
|
1640
1649
|
observations,
|
1641
1650
|
min_periods=observations,
|
1642
1651
|
)
|
1643
1652
|
|
1644
|
-
rcov = rolling.cov()
|
1653
|
+
rcov = rolling.cov(ddof=dlta_degr_freedms)
|
1645
1654
|
rcov = rcov.dropna()
|
1646
1655
|
|
1647
1656
|
rollbetaseries = rcov.iloc[:, asset_column].xs(
|
@@ -1692,17 +1701,15 @@ class OpenFrame(_CommonModel):
|
|
1692
1701
|
+ "_VS_"
|
1693
1702
|
+ cast(tuple[str, str], self.tsdf.iloc[:, second_column].name)[0]
|
1694
1703
|
)
|
1695
|
-
|
1704
|
+
first_series = (
|
1696
1705
|
self.tsdf.iloc[:, first_column]
|
1697
1706
|
.pct_change(fill_method=cast(str, None))[1:]
|
1698
1707
|
.rolling(observations, min_periods=observations)
|
1699
|
-
.corr(
|
1700
|
-
self.tsdf.iloc[:, second_column].pct_change(
|
1701
|
-
fill_method=cast(str, None),
|
1702
|
-
)[1:],
|
1703
|
-
)
|
1704
1708
|
)
|
1705
|
-
|
1709
|
+
second_series = self.tsdf.iloc[:, second_column].pct_change(
|
1710
|
+
fill_method=cast(str, None),
|
1711
|
+
)[1:]
|
1712
|
+
corrdf = first_series.corr(other=second_series).dropna().to_frame()
|
1706
1713
|
corrdf.columns = MultiIndex.from_arrays(
|
1707
1714
|
[
|
1708
1715
|
[corr_label],
|
@@ -63,6 +63,12 @@ class ReturnSimulation(BaseModel):
|
|
63
63
|
Mean annual standard deviation of the distribution
|
64
64
|
dframe: pandas.DataFrame
|
65
65
|
Pandas DataFrame object holding the resulting values
|
66
|
+
jumps_lamda: NonNegativeFloat, default: 0.0
|
67
|
+
This is the probability of a jump happening at each point in time
|
68
|
+
jumps_sigma: NonNegativeFloat, default: 0.0
|
69
|
+
This is the volatility of the jump size
|
70
|
+
jumps_mu: float, default: 0.0
|
71
|
+
This is the average jump size
|
66
72
|
seed: int, optional
|
67
73
|
Seed for random process initiation
|
68
74
|
randomizer: numpy.random.Generator, optional
|
@@ -76,6 +82,9 @@ class ReturnSimulation(BaseModel):
|
|
76
82
|
mean_annual_return: float
|
77
83
|
mean_annual_vol: PositiveFloat
|
78
84
|
dframe: DataFrame
|
85
|
+
jumps_lamda: NonNegativeFloat = 0.0
|
86
|
+
jumps_sigma: NonNegativeFloat = 0.0
|
87
|
+
jumps_mu: float = 0.0
|
79
88
|
seed: Optional[int] = None
|
80
89
|
randomizer: Optional[Generator] = None
|
81
90
|
|
@@ -387,6 +396,9 @@ class ReturnSimulation(BaseModel):
|
|
387
396
|
trading_days_in_year=trading_days_in_year,
|
388
397
|
mean_annual_return=mean_annual_return,
|
389
398
|
mean_annual_vol=mean_annual_vol,
|
399
|
+
jumps_lamda=jumps_lamda,
|
400
|
+
jumps_sigma=jumps_sigma,
|
401
|
+
jumps_mu=jumps_mu,
|
390
402
|
dframe=DataFrame(data=returns, dtype="float64"),
|
391
403
|
seed=seed,
|
392
404
|
randomizer=cls.randomizer,
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "openseries"
|
3
|
-
version = "1.4.
|
3
|
+
version = "1.4.12"
|
4
4
|
description = "Package for analyzing financial timeseries."
|
5
5
|
authors = ["Martin Karrin <martin.karrin@captor.se>"]
|
6
6
|
repository = "https://github.com/CaptorAB/OpenSeries"
|
@@ -50,14 +50,14 @@ statsmodels = ">=0.14.0,<1.0.0"
|
|
50
50
|
coverage = "^7.4.1"
|
51
51
|
coverage-badge = "^1.1.0"
|
52
52
|
mypy = "^1.8.0"
|
53
|
-
pre-commit = "^3.6.
|
54
|
-
pytest = "^8.0.
|
55
|
-
ruff = "^0.
|
53
|
+
pre-commit = "^3.6.1"
|
54
|
+
pytest = "^8.0.1"
|
55
|
+
ruff = "^0.2.2"
|
56
56
|
toml = "^0.10.2"
|
57
|
-
types-openpyxl = "^3.1.0.
|
57
|
+
types-openpyxl = "^3.1.0.20240205"
|
58
58
|
pandas-stubs = "^2.1.4.231227"
|
59
59
|
types-python-dateutil = "^2.8.19.20240106"
|
60
|
-
types-requests = "^2.31.0.
|
60
|
+
types-requests = "^2.31.0.20240218"
|
61
61
|
types-toml = "^0.10.8.7"
|
62
62
|
|
63
63
|
[build-system]
|
@@ -105,15 +105,17 @@ init_typed = true
|
|
105
105
|
warn_required_dynamic_aliases = true
|
106
106
|
|
107
107
|
[tool.ruff]
|
108
|
+
line-length = 87
|
109
|
+
|
110
|
+
[tool.ruff.lint]
|
108
111
|
select = ["ALL"]
|
109
112
|
ignore = ["D211", "D212", "TCH"]
|
110
113
|
fixable = ["ALL"]
|
111
|
-
line-length = 87
|
112
114
|
|
113
|
-
[tool.ruff.pylint]
|
115
|
+
[tool.ruff.lint.pylint]
|
114
116
|
max-args = 12
|
115
117
|
max-branches = 22
|
116
118
|
max-statements = 54
|
117
119
|
|
118
|
-
[tool.ruff.pyupgrade]
|
120
|
+
[tool.ruff.lint.pyupgrade]
|
119
121
|
keep-runtime-typing = true
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|