openseries 1.7.4__tar.gz → 1.7.6__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
  Metadata-Version: 2.1
2
2
  Name: openseries
3
- Version: 1.7.4
3
+ Version: 1.7.6
4
4
  Summary: Tools for analyzing financial timeseries.
5
5
  Home-page: https://github.com/CaptorAB/openseries
6
6
  License: BSD-3-Clause
@@ -20,16 +20,16 @@ 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,<=3.0.0)
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)
27
- Requires-Dist: pyarrow (>=14.0.2,<18.0.0)
27
+ Requires-Dist: pyarrow (>=14.0.2,<19.0.0)
28
28
  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
 
@@ -43,7 +43,11 @@ from ._risk import (
43
43
  _cvar_down_calc,
44
44
  _var_down_calc,
45
45
  )
46
- from .datefixer import date_offset_foll, holiday_calendar
46
+ from .datefixer import (
47
+ _do_resample_to_business_period_ends,
48
+ date_offset_foll,
49
+ holiday_calendar,
50
+ )
47
51
  from .load_plotly import load_plotly_dict
48
52
  from .types import (
49
53
  CountriesType,
@@ -52,6 +56,7 @@ from .types import (
52
56
  LiteralJsonOutput,
53
57
  LiteralLinePlotMode,
54
58
  LiteralNanMethod,
59
+ LiteralPandasReindexMethod,
55
60
  LiteralPlotlyJSlib,
56
61
  LiteralPlotlyOutput,
57
62
  LiteralQuantileInterp,
@@ -358,9 +363,17 @@ class _CommonModel(BaseModel):
358
363
  Most negative month
359
364
 
360
365
  """
366
+ method: LiteralPandasReindexMethod = "nearest"
367
+ countries = "SE"
361
368
  wmdf = self.tsdf.copy()
369
+ dates = _do_resample_to_business_period_ends(
370
+ data=wmdf,
371
+ freq="BME",
372
+ countries=countries,
373
+ )
374
+ wmdf = wmdf.reindex(index=[deyt.date() for deyt in dates], method=method)
362
375
  wmdf.index = DatetimeIndex(wmdf.index)
363
- result = wmdf.resample("BME").last().pct_change().min()
376
+ result = wmdf.ffill().pct_change().min()
364
377
 
365
378
  if self.tsdf.shape[1] == 1:
366
379
  return float(result.iloc[0])
@@ -1285,19 +1298,24 @@ class _CommonModel(BaseModel):
1285
1298
  if drift_adjust:
1286
1299
  imp_vol = (-sqrt(time_factor) / norm.ppf(level)) * (
1287
1300
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1301
+ .ffill()
1288
1302
  .pct_change()
1289
1303
  .quantile(1 - level, interpolation=interpolation)
1290
1304
  - self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1305
+ .ffill()
1291
1306
  .pct_change()
1292
1307
  .sum()
1293
1308
  / len(
1294
- self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(),
1309
+ self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1310
+ .ffill()
1311
+ .pct_change(),
1295
1312
  )
1296
1313
  )
1297
1314
  else:
1298
1315
  imp_vol = (
1299
1316
  -sqrt(time_factor)
1300
1317
  * self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1318
+ .ffill()
1301
1319
  .pct_change()
1302
1320
  .quantile(1 - level, interpolation=interpolation)
1303
1321
  / norm.ppf(level)
@@ -1361,6 +1379,7 @@ class _CommonModel(BaseModel):
1361
1379
  cvar_df = self.tsdf.loc[cast(int, earlier) : cast(int, later)].copy(deep=True)
1362
1380
  result = [
1363
1381
  cvar_df.loc[:, x] # type: ignore[call-overload,index]
1382
+ .ffill()
1364
1383
  .pct_change()
1365
1384
  .sort_values()
1366
1385
  .iloc[
@@ -1368,6 +1387,7 @@ class _CommonModel(BaseModel):
1368
1387
  ceil(
1369
1388
  (1 - level)
1370
1389
  * cvar_df.loc[:, x] # type: ignore[index]
1390
+ .ffill()
1371
1391
  .pct_change()
1372
1392
  .count(),
1373
1393
  ),
@@ -1428,6 +1448,7 @@ class _CommonModel(BaseModel):
1428
1448
  )
1429
1449
  how_many = (
1430
1450
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1451
+ .ffill()
1431
1452
  .pct_change()
1432
1453
  .count(numeric_only=True)
1433
1454
  )
@@ -1443,6 +1464,7 @@ class _CommonModel(BaseModel):
1443
1464
 
1444
1465
  dddf = (
1445
1466
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1467
+ .ffill()
1446
1468
  .pct_change()
1447
1469
  .sub(min_accepted_return / time_factor)
1448
1470
  )
@@ -1546,6 +1568,7 @@ class _CommonModel(BaseModel):
1546
1568
  )
1547
1569
  result: NDArray[float64] = skew(
1548
1570
  a=self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1571
+ .ffill()
1549
1572
  .pct_change()
1550
1573
  .to_numpy(),
1551
1574
  bias=True,
@@ -1593,7 +1616,7 @@ class _CommonModel(BaseModel):
1593
1616
  to_dt=to_date,
1594
1617
  )
1595
1618
  result: NDArray[float64] = kurtosis(
1596
- self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(),
1619
+ self.tsdf.loc[cast(int, earlier) : cast(int, later)].ffill().pct_change(),
1597
1620
  fisher=True,
1598
1621
  bias=True,
1599
1622
  nan_policy="omit",
@@ -1689,13 +1712,21 @@ class _CommonModel(BaseModel):
1689
1712
  )
1690
1713
  pos = (
1691
1714
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1715
+ .ffill()
1692
1716
  .pct_change()[1:][
1693
- self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change()[1:]
1717
+ self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1718
+ .ffill()
1719
+ .pct_change()[1:]
1694
1720
  > zero
1695
1721
  ]
1696
1722
  .count()
1697
1723
  )
1698
- tot = self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change().count()
1724
+ tot = (
1725
+ self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1726
+ .ffill()
1727
+ .pct_change()
1728
+ .count()
1729
+ )
1699
1730
  share = pos / tot
1700
1731
  if self.tsdf.shape[1] == 1:
1701
1732
  return float(share.iloc[0])
@@ -1871,7 +1902,9 @@ class _CommonModel(BaseModel):
1871
1902
  from_dt=from_date,
1872
1903
  to_dt=to_date,
1873
1904
  )
1874
- retdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change()
1905
+ retdf = (
1906
+ self.tsdf.loc[cast(int, earlier) : cast(int, later)].ffill().pct_change()
1907
+ )
1875
1908
  pos = retdf[retdf > min_accepted_return].sub(min_accepted_return).sum()
1876
1909
  neg = retdf[retdf < min_accepted_return].sub(min_accepted_return).sum()
1877
1910
  ratio = pos / -neg
@@ -1959,7 +1992,7 @@ class _CommonModel(BaseModel):
1959
1992
  period = "-".join([str(year), str(month).zfill(2)])
1960
1993
  vrdf = self.tsdf.copy()
1961
1994
  vrdf.index = DatetimeIndex(vrdf.index)
1962
- resultdf = DataFrame(vrdf.pct_change())
1995
+ resultdf = DataFrame(vrdf.ffill().pct_change())
1963
1996
  result = resultdf.loc[period] + 1
1964
1997
  cal_period = result.cumprod(axis="index").iloc[-1] - 1
1965
1998
  if self.tsdf.shape[1] == 1:
@@ -2011,6 +2044,7 @@ class _CommonModel(BaseModel):
2011
2044
  )
2012
2045
  result = (
2013
2046
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
2047
+ .ffill()
2014
2048
  .pct_change()
2015
2049
  .quantile(1 - level, interpolation=interpolation)
2016
2050
  )
@@ -2059,6 +2093,7 @@ class _CommonModel(BaseModel):
2059
2093
  )
2060
2094
  result = (
2061
2095
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
2096
+ .ffill()
2062
2097
  .pct_change()
2063
2098
  .rolling(observations, min_periods=observations)
2064
2099
  .sum()
@@ -2105,7 +2140,9 @@ class _CommonModel(BaseModel):
2105
2140
  from_dt=from_date,
2106
2141
  to_dt=to_date,
2107
2142
  )
2108
- zscframe = self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change()
2143
+ zscframe = (
2144
+ self.tsdf.loc[cast(int, earlier) : cast(int, later)].ffill().pct_change()
2145
+ )
2109
2146
  result = (zscframe.iloc[-1] - zscframe.mean()) / zscframe.std()
2110
2147
 
2111
2148
  if self.tsdf.shape[1] == 1:
@@ -2174,6 +2211,7 @@ class _CommonModel(BaseModel):
2174
2211
  ret_label = cast(tuple[str], self.tsdf.iloc[:, column].name)[0]
2175
2212
  retseries = (
2176
2213
  self.tsdf.iloc[:, column]
2214
+ .ffill()
2177
2215
  .pct_change()
2178
2216
  .rolling(observations, min_periods=observations)
2179
2217
  .sum()
@@ -2251,7 +2289,7 @@ class _CommonModel(BaseModel):
2251
2289
  else:
2252
2290
  time_factor = self.periods_in_a_year
2253
2291
  vol_label = cast(tuple[str, ValueType], self.tsdf.iloc[:, column].name)[0]
2254
- dframe = self.tsdf.iloc[:, column].pct_change()
2292
+ dframe = self.tsdf.iloc[:, column].ffill().pct_change()
2255
2293
  volseries = dframe.rolling(
2256
2294
  observations,
2257
2295
  min_periods=observations,
@@ -602,9 +602,13 @@ class OpenFrame(_CommonModel):
602
602
  Correlation matrix
603
603
 
604
604
  """
605
- corr_matrix = self.tsdf.pct_change().corr(
606
- method="pearson",
607
- min_periods=1,
605
+ corr_matrix = (
606
+ self.tsdf.ffill()
607
+ .pct_change()
608
+ .corr(
609
+ method="pearson",
610
+ min_periods=1,
611
+ )
608
612
  )
609
613
  corr_matrix.columns = corr_matrix.columns.droplevel(level=1)
610
614
  corr_matrix.index = corr_matrix.index.droplevel(level=1)
@@ -829,7 +833,7 @@ class OpenFrame(_CommonModel):
829
833
  ]
830
834
  relative = 1.0 + longdf - shortdf
831
835
  vol = float(
832
- relative.pct_change().std() * sqrt(time_factor),
836
+ relative.ffill().pct_change().std() * sqrt(time_factor),
833
837
  )
834
838
  terrors.append(vol)
835
839
 
@@ -919,10 +923,10 @@ class OpenFrame(_CommonModel):
919
923
  ]
920
924
  relative = 1.0 + longdf - shortdf
921
925
  ret = float(
922
- relative.pct_change().mean() * time_factor,
926
+ relative.ffill().pct_change().mean() * time_factor,
923
927
  )
924
928
  vol = float(
925
- relative.pct_change().std() * sqrt(time_factor),
929
+ relative.ffill().pct_change().std() * sqrt(time_factor),
926
930
  )
927
931
  ratios.append(ret / vol)
928
932
 
@@ -1021,16 +1025,18 @@ class OpenFrame(_CommonModel):
1021
1025
  msg = "ratio must be one of 'up', 'down' or 'both'."
1022
1026
  if ratio == "up":
1023
1027
  uparray = (
1024
- longdf.pct_change()[
1025
- shortdf.pct_change().to_numpy() > loss_limit
1028
+ longdf.ffill()
1029
+ .pct_change()[
1030
+ shortdf.ffill().pct_change().to_numpy() > loss_limit
1026
1031
  ]
1027
1032
  .add(1)
1028
1033
  .to_numpy()
1029
1034
  )
1030
1035
  up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
1031
1036
  upidxarray = (
1032
- shortdf.pct_change()[
1033
- shortdf.pct_change().to_numpy() > loss_limit
1037
+ shortdf.ffill()
1038
+ .pct_change()[
1039
+ shortdf.ffill().pct_change().to_numpy() > loss_limit
1034
1040
  ]
1035
1041
  .add(1)
1036
1042
  .to_numpy()
@@ -1041,8 +1047,9 @@ class OpenFrame(_CommonModel):
1041
1047
  ratios.append(up_rtrn / up_idx_return)
1042
1048
  elif ratio == "down":
1043
1049
  downarray = (
1044
- longdf.pct_change()[
1045
- shortdf.pct_change().to_numpy() < loss_limit
1050
+ longdf.ffill()
1051
+ .pct_change()[
1052
+ shortdf.ffill().pct_change().to_numpy() < loss_limit
1046
1053
  ]
1047
1054
  .add(1)
1048
1055
  .to_numpy()
@@ -1051,8 +1058,9 @@ class OpenFrame(_CommonModel):
1051
1058
  downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
1052
1059
  )
1053
1060
  downidxarray = (
1054
- shortdf.pct_change()[
1055
- shortdf.pct_change().to_numpy() < loss_limit
1061
+ shortdf.ffill()
1062
+ .pct_change()[
1063
+ shortdf.ffill().pct_change().to_numpy() < loss_limit
1056
1064
  ]
1057
1065
  .add(1)
1058
1066
  .to_numpy()
@@ -1064,16 +1072,18 @@ class OpenFrame(_CommonModel):
1064
1072
  ratios.append(down_return / down_idx_return)
1065
1073
  elif ratio == "both":
1066
1074
  uparray = (
1067
- longdf.pct_change()[
1068
- shortdf.pct_change().to_numpy() > loss_limit
1075
+ longdf.ffill()
1076
+ .pct_change()[
1077
+ shortdf.ffill().pct_change().to_numpy() > loss_limit
1069
1078
  ]
1070
1079
  .add(1)
1071
1080
  .to_numpy()
1072
1081
  )
1073
1082
  up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
1074
1083
  upidxarray = (
1075
- shortdf.pct_change()[
1076
- shortdf.pct_change().to_numpy() > loss_limit
1084
+ shortdf.ffill()
1085
+ .pct_change()[
1086
+ shortdf.ffill().pct_change().to_numpy() > loss_limit
1077
1087
  ]
1078
1088
  .add(1)
1079
1089
  .to_numpy()
@@ -1082,8 +1092,9 @@ class OpenFrame(_CommonModel):
1082
1092
  upidxarray.prod() ** (1 / (len(upidxarray) / time_factor)) - 1
1083
1093
  )
1084
1094
  downarray = (
1085
- longdf.pct_change()[
1086
- shortdf.pct_change().to_numpy() < loss_limit
1095
+ longdf.ffill()
1096
+ .pct_change()[
1097
+ shortdf.ffill().pct_change().to_numpy() < loss_limit
1087
1098
  ]
1088
1099
  .add(1)
1089
1100
  .to_numpy()
@@ -1092,8 +1103,9 @@ class OpenFrame(_CommonModel):
1092
1103
  downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
1093
1104
  )
1094
1105
  downidxarray = (
1095
- shortdf.pct_change()[
1096
- shortdf.pct_change().to_numpy() < loss_limit
1106
+ shortdf.ffill()
1107
+ .pct_change()[
1108
+ shortdf.ffill().pct_change().to_numpy() < loss_limit
1097
1109
  ]
1098
1110
  .add(1)
1099
1111
  .to_numpy()
@@ -1499,11 +1511,14 @@ class OpenFrame(_CommonModel):
1499
1511
  )
1500
1512
 
1501
1513
  retseries = (
1502
- relative.pct_change().rolling(observations, min_periods=observations).sum()
1514
+ relative.ffill()
1515
+ .pct_change()
1516
+ .rolling(observations, min_periods=observations)
1517
+ .sum()
1503
1518
  )
1504
1519
  retdf = retseries.dropna().to_frame()
1505
1520
 
1506
- voldf = relative.pct_change().rolling(
1521
+ voldf = relative.ffill().pct_change().rolling(
1507
1522
  observations,
1508
1523
  min_periods=observations,
1509
1524
  ).std() * sqrt(time_factor)
@@ -1547,9 +1562,13 @@ class OpenFrame(_CommonModel):
1547
1562
  asset_label = cast(tuple[str, str], self.tsdf.iloc[:, asset_column].name)[0]
1548
1563
  beta_label = f"{asset_label} / {market_label}"
1549
1564
 
1550
- rolling = self.tsdf.pct_change().rolling(
1551
- observations,
1552
- min_periods=observations,
1565
+ rolling = (
1566
+ self.tsdf.ffill()
1567
+ .pct_change()
1568
+ .rolling(
1569
+ observations,
1570
+ min_periods=observations,
1571
+ )
1553
1572
  )
1554
1573
 
1555
1574
  rcov = rolling.cov(ddof=dlta_degr_freedms)
@@ -1604,10 +1623,11 @@ class OpenFrame(_CommonModel):
1604
1623
  )
1605
1624
  first_series = (
1606
1625
  self.tsdf.iloc[:, first_column]
1626
+ .ffill()
1607
1627
  .pct_change()[1:]
1608
1628
  .rolling(observations, min_periods=observations)
1609
1629
  )
1610
- second_series = self.tsdf.iloc[:, second_column].pct_change()[1:]
1630
+ second_series = self.tsdf.iloc[:, second_column].ffill().pct_change()[1:]
1611
1631
  corrdf = first_series.corr(other=second_series).dropna().to_frame()
1612
1632
  corrdf.columns = MultiIndex.from_arrays(
1613
1633
  [
@@ -308,7 +308,7 @@ def efficient_frontier( # noqa: C901
308
308
 
309
309
  if tweak:
310
310
  limit_tweak = 0.001
311
- line_df["stdev_diff"] = line_df.stdev.pct_change()
311
+ line_df["stdev_diff"] = line_df.stdev.ffill().pct_change()
312
312
  line_df = line_df.loc[line_df.stdev_diff.abs() > limit_tweak]
313
313
  line_df = line_df.drop(columns="stdev_diff")
314
314
 
@@ -687,7 +687,7 @@ class OpenTimeSeries(_CommonModel):
687
687
  returns_input = True
688
688
  else:
689
689
  values = [cast(float, self.tsdf.iloc[0, 0])]
690
- ra_df = self.tsdf.pct_change()
690
+ ra_df = self.tsdf.ffill().pct_change()
691
691
  returns_input = False
692
692
  ra_df = ra_df.dropna()
693
693
 
@@ -122,7 +122,9 @@ class ReturnSimulation(BaseModel):
122
122
  """
123
123
  return cast(
124
124
  float,
125
- (self.results.pct_change().mean() * self.trading_days_in_year).iloc[0],
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
- (self.results.pct_change().std() * sqrt(self.trading_days_in_year)).iloc[
141
- 0
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
  [tool.poetry]
2
2
  name = "openseries"
3
- version = "1.7.4"
3
+ version = "1.7.6"
4
4
  description = "Tools for analyzing financial timeseries."
5
5
  authors = ["Martin Karrin <martin.karrin@captor.se>"]
6
6
  repository = "https://github.com/CaptorAB/openseries"
@@ -34,32 +34,32 @@ keywords = [
34
34
  [tool.poetry.dependencies]
35
35
  python = ">=3.10,<3.13"
36
36
  holidays = ">=0.30,<1.0"
37
- numpy = ">=1.23.2,<=3.0.0"
37
+ numpy = ">=1.23.2,<3.0.0"
38
38
  openpyxl = ">=3.1.2,<4.0.0"
39
39
  pandas = ">=2.1.2,<3.0.0"
40
40
  plotly = ">=5.18.0,<6.0.0"
41
- pyarrow = ">=14.0.2,<18.0.0"
41
+ pyarrow = ">=14.0.2,<19.0.0"
42
42
  pydantic = ">=2.5.2,<3.0.0"
43
43
  python-dateutil = ">=2.8.2,<3.0.0"
44
44
  requests = ">=2.20.0,<3.0.0"
45
45
  scipy = ">=1.11.4,<2.0.0"
46
- statsmodels = ">=0.14.0,<1.0.0"
46
+ statsmodels = ">=0.14.0,!=0.14.2,<1.0.0"
47
47
 
48
48
  [tool.poetry.group.dev.dependencies]
49
49
  black = ">=24.4.2,<25.0.0"
50
- coverage = ">=7.6.0,<8.0.0"
50
+ coverage = "^7.6.4"
51
51
  genbadge = {version = ">=1.1.1,<2.0.0", extras = ["coverage"]}
52
- mypy = "^1.11.2"
52
+ mypy = "^1.13.0"
53
53
  pandas-stubs = ">=2.1.2,<3.0.0"
54
54
  pre-commit = ">=3.7.1,<6.0.0"
55
55
  pytest = ">=8.2.2,<9.0.0"
56
- ruff = "^0.6.9"
56
+ ruff = "^0.7.1"
57
57
  types-openpyxl = ">=3.1.2,<4.0.0"
58
58
  types-python-dateutil = ">=2.8.2,<3.0.0"
59
59
  types-requests = ">=2.20.0,<3.0.0"
60
60
 
61
61
  [build-system]
62
- requires = ["poetry-core>=1.8.3"]
62
+ requires = ["poetry-core>=1.8.4"]
63
63
  build-backend = "poetry.core.masonry.api"
64
64
 
65
65
  [tool.coverage.run]
File without changes
File without changes