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.
@@ -1,6 +1,6 @@
1
1
  # BSD 3-Clause License
2
2
 
3
- ## Copyright (c) 2023, Captor Fund Management AB
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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openseries
3
- Version: 1.4.10
3
+ Version: 1.4.12
4
4
  Summary: Package for analyzing financial timeseries.
5
5
  Home-page: https://github.com/CaptorAB/OpenSeries
6
6
  License: BSD-3-Clause
@@ -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 Inf, float64, isnan, log, maximum, sqrt
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)] = -Inf
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
- if (
1518
- zero in self.tsdf.loc[earlier].tolist()
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)] = NaN
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
- msg,
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
- up_return = (
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(up_return / up_idx_return)
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
- up_return = (
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
- (up_return / up_idx_return) / (down_return / down_idx_return),
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=1)
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=1)
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
- corrseries = (
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
- corrdf = corrseries.dropna().to_frame()
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.10"
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.0"
54
- pytest = "^8.0.0"
55
- ruff = "^0.1.14"
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.20240106"
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.20240125"
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