openseries 1.7.3__tar.gz → 1.7.5__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.3
3
+ Version: 1.7.5
4
4
  Summary: Tools for analyzing financial timeseries.
5
5
  Home-page: https://github.com/CaptorAB/openseries
6
6
  License: BSD-3-Clause
@@ -20,7 +20,7 @@ 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)
@@ -29,7 +29,7 @@ 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
 
@@ -59,6 +59,7 @@ from .types import (
59
59
  )
60
60
 
61
61
 
62
+ # noinspection PyTypeChecker
62
63
  class _CommonModel(BaseModel):
63
64
  """Declare _CommonModel."""
64
65
 
@@ -359,7 +360,7 @@ class _CommonModel(BaseModel):
359
360
  """
360
361
  wmdf = self.tsdf.copy()
361
362
  wmdf.index = DatetimeIndex(wmdf.index)
362
- result = wmdf.resample("BME").last().pct_change().min()
363
+ result = wmdf.resample("BME").last().ffill().pct_change().min()
363
364
 
364
365
  if self.tsdf.shape[1] == 1:
365
366
  return float(result.iloc[0])
@@ -680,7 +681,7 @@ class _CommonModel(BaseModel):
680
681
  output.append(dict(itemdata))
681
682
 
682
683
  with dirpath.joinpath(filename).open(mode="w", encoding="utf-8") as jsonfile:
683
- dump(output, jsonfile, indent=2, sort_keys=False)
684
+ dump(obj=output, fp=jsonfile, indent=2, sort_keys=False)
684
685
 
685
686
  return output
686
687
 
@@ -1027,7 +1028,10 @@ class _CommonModel(BaseModel):
1027
1028
  time_factor = how_many / fraction
1028
1029
 
1029
1030
  result = (
1030
- self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change().mean()
1031
+ self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1032
+ .ffill()
1033
+ .pct_change()
1034
+ .mean()
1031
1035
  * time_factor
1032
1036
  )
1033
1037
 
@@ -1085,7 +1089,7 @@ class _CommonModel(BaseModel):
1085
1089
  time_factor = how_many / fraction
1086
1090
 
1087
1091
  data = self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1088
- result = data.pct_change().std().mul(sqrt(time_factor))
1092
+ result = data.ffill().pct_change().std().mul(sqrt(time_factor))
1089
1093
 
1090
1094
  if self.tsdf.shape[1] == 1:
1091
1095
  return float(cast(SupportsFloat, result.iloc[0]))
@@ -1281,19 +1285,24 @@ class _CommonModel(BaseModel):
1281
1285
  if drift_adjust:
1282
1286
  imp_vol = (-sqrt(time_factor) / norm.ppf(level)) * (
1283
1287
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1288
+ .ffill()
1284
1289
  .pct_change()
1285
1290
  .quantile(1 - level, interpolation=interpolation)
1286
1291
  - self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1292
+ .ffill()
1287
1293
  .pct_change()
1288
1294
  .sum()
1289
1295
  / len(
1290
- self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(),
1296
+ self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1297
+ .ffill()
1298
+ .pct_change(),
1291
1299
  )
1292
1300
  )
1293
1301
  else:
1294
1302
  imp_vol = (
1295
1303
  -sqrt(time_factor)
1296
1304
  * self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1305
+ .ffill()
1297
1306
  .pct_change()
1298
1307
  .quantile(1 - level, interpolation=interpolation)
1299
1308
  / norm.ppf(level)
@@ -1357,6 +1366,7 @@ class _CommonModel(BaseModel):
1357
1366
  cvar_df = self.tsdf.loc[cast(int, earlier) : cast(int, later)].copy(deep=True)
1358
1367
  result = [
1359
1368
  cvar_df.loc[:, x] # type: ignore[call-overload,index]
1369
+ .ffill()
1360
1370
  .pct_change()
1361
1371
  .sort_values()
1362
1372
  .iloc[
@@ -1364,6 +1374,7 @@ class _CommonModel(BaseModel):
1364
1374
  ceil(
1365
1375
  (1 - level)
1366
1376
  * cvar_df.loc[:, x] # type: ignore[index]
1377
+ .ffill()
1367
1378
  .pct_change()
1368
1379
  .count(),
1369
1380
  ),
@@ -1424,6 +1435,7 @@ class _CommonModel(BaseModel):
1424
1435
  )
1425
1436
  how_many = (
1426
1437
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1438
+ .ffill()
1427
1439
  .pct_change()
1428
1440
  .count(numeric_only=True)
1429
1441
  )
@@ -1439,6 +1451,7 @@ class _CommonModel(BaseModel):
1439
1451
 
1440
1452
  dddf = (
1441
1453
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1454
+ .ffill()
1442
1455
  .pct_change()
1443
1456
  .sub(min_accepted_return / time_factor)
1444
1457
  )
@@ -1542,6 +1555,7 @@ class _CommonModel(BaseModel):
1542
1555
  )
1543
1556
  result: NDArray[float64] = skew(
1544
1557
  a=self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1558
+ .ffill()
1545
1559
  .pct_change()
1546
1560
  .to_numpy(),
1547
1561
  bias=True,
@@ -1589,7 +1603,7 @@ class _CommonModel(BaseModel):
1589
1603
  to_dt=to_date,
1590
1604
  )
1591
1605
  result: NDArray[float64] = kurtosis(
1592
- self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(),
1606
+ self.tsdf.loc[cast(int, earlier) : cast(int, later)].ffill().pct_change(),
1593
1607
  fisher=True,
1594
1608
  bias=True,
1595
1609
  nan_policy="omit",
@@ -1685,13 +1699,21 @@ class _CommonModel(BaseModel):
1685
1699
  )
1686
1700
  pos = (
1687
1701
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1702
+ .ffill()
1688
1703
  .pct_change()[1:][
1689
- self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change()[1:]
1704
+ self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1705
+ .ffill()
1706
+ .pct_change()[1:]
1690
1707
  > zero
1691
1708
  ]
1692
1709
  .count()
1693
1710
  )
1694
- tot = self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change().count()
1711
+ tot = (
1712
+ self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1713
+ .ffill()
1714
+ .pct_change()
1715
+ .count()
1716
+ )
1695
1717
  share = pos / tot
1696
1718
  if self.tsdf.shape[1] == 1:
1697
1719
  return float(share.iloc[0])
@@ -1867,7 +1889,9 @@ class _CommonModel(BaseModel):
1867
1889
  from_dt=from_date,
1868
1890
  to_dt=to_date,
1869
1891
  )
1870
- retdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change()
1892
+ retdf = (
1893
+ self.tsdf.loc[cast(int, earlier) : cast(int, later)].ffill().pct_change()
1894
+ )
1871
1895
  pos = retdf[retdf > min_accepted_return].sub(min_accepted_return).sum()
1872
1896
  neg = retdf[retdf < min_accepted_return].sub(min_accepted_return).sum()
1873
1897
  ratio = pos / -neg
@@ -1955,7 +1979,7 @@ class _CommonModel(BaseModel):
1955
1979
  period = "-".join([str(year), str(month).zfill(2)])
1956
1980
  vrdf = self.tsdf.copy()
1957
1981
  vrdf.index = DatetimeIndex(vrdf.index)
1958
- resultdf = DataFrame(vrdf.pct_change())
1982
+ resultdf = DataFrame(vrdf.ffill().pct_change())
1959
1983
  result = resultdf.loc[period] + 1
1960
1984
  cal_period = result.cumprod(axis="index").iloc[-1] - 1
1961
1985
  if self.tsdf.shape[1] == 1:
@@ -2007,6 +2031,7 @@ class _CommonModel(BaseModel):
2007
2031
  )
2008
2032
  result = (
2009
2033
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
2034
+ .ffill()
2010
2035
  .pct_change()
2011
2036
  .quantile(1 - level, interpolation=interpolation)
2012
2037
  )
@@ -2055,6 +2080,7 @@ class _CommonModel(BaseModel):
2055
2080
  )
2056
2081
  result = (
2057
2082
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
2083
+ .ffill()
2058
2084
  .pct_change()
2059
2085
  .rolling(observations, min_periods=observations)
2060
2086
  .sum()
@@ -2101,7 +2127,9 @@ class _CommonModel(BaseModel):
2101
2127
  from_dt=from_date,
2102
2128
  to_dt=to_date,
2103
2129
  )
2104
- zscframe = self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change()
2130
+ zscframe = (
2131
+ self.tsdf.loc[cast(int, earlier) : cast(int, later)].ffill().pct_change()
2132
+ )
2105
2133
  result = (zscframe.iloc[-1] - zscframe.mean()) / zscframe.std()
2106
2134
 
2107
2135
  if self.tsdf.shape[1] == 1:
@@ -2170,6 +2198,7 @@ class _CommonModel(BaseModel):
2170
2198
  ret_label = cast(tuple[str], self.tsdf.iloc[:, column].name)[0]
2171
2199
  retseries = (
2172
2200
  self.tsdf.iloc[:, column]
2201
+ .ffill()
2173
2202
  .pct_change()
2174
2203
  .rolling(observations, min_periods=observations)
2175
2204
  .sum()
@@ -2247,7 +2276,7 @@ class _CommonModel(BaseModel):
2247
2276
  else:
2248
2277
  time_factor = self.periods_in_a_year
2249
2278
  vol_label = cast(tuple[str, ValueType], self.tsdf.iloc[:, column].name)[0]
2250
- dframe = self.tsdf.iloc[:, column].pct_change()
2279
+ dframe = self.tsdf.iloc[:, column].ffill().pct_change()
2251
2280
  volseries = dframe.rolling(
2252
2281
  observations,
2253
2282
  min_periods=observations,
@@ -14,7 +14,6 @@ if TYPE_CHECKING:
14
14
  import statsmodels.api as sm # type: ignore[import-untyped,unused-ignore]
15
15
  from numpy import (
16
16
  cov,
17
- cumprod,
18
17
  divide,
19
18
  isinf,
20
19
  log,
@@ -336,11 +335,12 @@ class OpenFrame(_CommonModel):
336
335
  The returns of the values in the series
337
336
 
338
337
  """
339
- self.tsdf = self.tsdf.pct_change()
340
- self.tsdf.iloc[0] = 0
338
+ returns = self.tsdf.ffill().pct_change()
339
+ returns.iloc[0] = 0
341
340
  new_labels = [ValueType.RTRN] * self.item_count
342
341
  arrays = [self.tsdf.columns.get_level_values(0), new_labels]
343
- self.tsdf.columns = MultiIndex.from_arrays(arrays)
342
+ returns.columns = MultiIndex.from_arrays(arrays=arrays)
343
+ self.tsdf = returns.copy()
344
344
  return self
345
345
 
346
346
  def value_to_diff(self: Self, periods: int = 1) -> Self:
@@ -374,14 +374,20 @@ class OpenFrame(_CommonModel):
374
374
  An OpenFrame object
375
375
 
376
376
  """
377
- if any(
378
- x == ValueType.PRICE
379
- for x in self.tsdf.columns.get_level_values(1).to_numpy()
380
- ):
381
- self.value_to_ret()
377
+ vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
378
+ if not any(vtypes):
379
+ returns = self.tsdf.ffill().pct_change()
380
+ returns.iloc[0] = 0
381
+ elif all(vtypes):
382
+ returns = self.tsdf.copy()
383
+ returns.iloc[0] = 0
384
+ else:
385
+ msg = "Mix of series types will give inconsistent results"
386
+ raise ValueError(msg)
387
+
388
+ returns = returns.add(1.0)
389
+ self.tsdf = returns.cumprod(axis=0) / returns.iloc[0]
382
390
 
383
- self.tsdf = self.tsdf.add(1.0)
384
- self.tsdf = self.tsdf.apply(cumprod, axis="index") / self.tsdf.iloc[0]
385
391
  new_labels = [ValueType.PRICE] * self.item_count
386
392
  arrays = [self.tsdf.columns.get_level_values(0), new_labels]
387
393
  self.tsdf.columns = MultiIndex.from_arrays(arrays)
@@ -453,8 +459,15 @@ class OpenFrame(_CommonModel):
453
459
  method=method,
454
460
  )
455
461
 
462
+ arrays = [
463
+ self.tsdf.columns.get_level_values(0),
464
+ self.tsdf.columns.get_level_values(1),
465
+ ]
466
+
456
467
  self._set_tsdf()
457
468
 
469
+ self.tsdf.columns = MultiIndex.from_arrays(arrays)
470
+
458
471
  return self
459
472
 
460
473
  def ewma_risk(
@@ -589,9 +602,13 @@ class OpenFrame(_CommonModel):
589
602
  Correlation matrix
590
603
 
591
604
  """
592
- corr_matrix = self.tsdf.pct_change().corr(
593
- method="pearson",
594
- min_periods=1,
605
+ corr_matrix = (
606
+ self.tsdf.ffill()
607
+ .pct_change()
608
+ .corr(
609
+ method="pearson",
610
+ min_periods=1,
611
+ )
595
612
  )
596
613
  corr_matrix.columns = corr_matrix.columns.droplevel(level=1)
597
614
  corr_matrix.index = corr_matrix.index.droplevel(level=1)
@@ -816,7 +833,7 @@ class OpenFrame(_CommonModel):
816
833
  ]
817
834
  relative = 1.0 + longdf - shortdf
818
835
  vol = float(
819
- relative.pct_change().std() * sqrt(time_factor),
836
+ relative.ffill().pct_change().std() * sqrt(time_factor),
820
837
  )
821
838
  terrors.append(vol)
822
839
 
@@ -906,10 +923,10 @@ class OpenFrame(_CommonModel):
906
923
  ]
907
924
  relative = 1.0 + longdf - shortdf
908
925
  ret = float(
909
- relative.pct_change().mean() * time_factor,
926
+ relative.ffill().pct_change().mean() * time_factor,
910
927
  )
911
928
  vol = float(
912
- relative.pct_change().std() * sqrt(time_factor),
929
+ relative.ffill().pct_change().std() * sqrt(time_factor),
913
930
  )
914
931
  ratios.append(ret / vol)
915
932
 
@@ -1008,16 +1025,18 @@ class OpenFrame(_CommonModel):
1008
1025
  msg = "ratio must be one of 'up', 'down' or 'both'."
1009
1026
  if ratio == "up":
1010
1027
  uparray = (
1011
- longdf.pct_change()[
1012
- shortdf.pct_change().to_numpy() > loss_limit
1028
+ longdf.ffill()
1029
+ .pct_change()[
1030
+ shortdf.ffill().pct_change().to_numpy() > loss_limit
1013
1031
  ]
1014
1032
  .add(1)
1015
1033
  .to_numpy()
1016
1034
  )
1017
1035
  up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
1018
1036
  upidxarray = (
1019
- shortdf.pct_change()[
1020
- shortdf.pct_change().to_numpy() > loss_limit
1037
+ shortdf.ffill()
1038
+ .pct_change()[
1039
+ shortdf.ffill().pct_change().to_numpy() > loss_limit
1021
1040
  ]
1022
1041
  .add(1)
1023
1042
  .to_numpy()
@@ -1028,8 +1047,9 @@ class OpenFrame(_CommonModel):
1028
1047
  ratios.append(up_rtrn / up_idx_return)
1029
1048
  elif ratio == "down":
1030
1049
  downarray = (
1031
- longdf.pct_change()[
1032
- shortdf.pct_change().to_numpy() < loss_limit
1050
+ longdf.ffill()
1051
+ .pct_change()[
1052
+ shortdf.ffill().pct_change().to_numpy() < loss_limit
1033
1053
  ]
1034
1054
  .add(1)
1035
1055
  .to_numpy()
@@ -1038,8 +1058,9 @@ class OpenFrame(_CommonModel):
1038
1058
  downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
1039
1059
  )
1040
1060
  downidxarray = (
1041
- shortdf.pct_change()[
1042
- shortdf.pct_change().to_numpy() < loss_limit
1061
+ shortdf.ffill()
1062
+ .pct_change()[
1063
+ shortdf.ffill().pct_change().to_numpy() < loss_limit
1043
1064
  ]
1044
1065
  .add(1)
1045
1066
  .to_numpy()
@@ -1051,16 +1072,18 @@ class OpenFrame(_CommonModel):
1051
1072
  ratios.append(down_return / down_idx_return)
1052
1073
  elif ratio == "both":
1053
1074
  uparray = (
1054
- longdf.pct_change()[
1055
- shortdf.pct_change().to_numpy() > loss_limit
1075
+ longdf.ffill()
1076
+ .pct_change()[
1077
+ shortdf.ffill().pct_change().to_numpy() > loss_limit
1056
1078
  ]
1057
1079
  .add(1)
1058
1080
  .to_numpy()
1059
1081
  )
1060
1082
  up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
1061
1083
  upidxarray = (
1062
- shortdf.pct_change()[
1063
- shortdf.pct_change().to_numpy() > loss_limit
1084
+ shortdf.ffill()
1085
+ .pct_change()[
1086
+ shortdf.ffill().pct_change().to_numpy() > loss_limit
1064
1087
  ]
1065
1088
  .add(1)
1066
1089
  .to_numpy()
@@ -1069,8 +1092,9 @@ class OpenFrame(_CommonModel):
1069
1092
  upidxarray.prod() ** (1 / (len(upidxarray) / time_factor)) - 1
1070
1093
  )
1071
1094
  downarray = (
1072
- longdf.pct_change()[
1073
- shortdf.pct_change().to_numpy() < loss_limit
1095
+ longdf.ffill()
1096
+ .pct_change()[
1097
+ shortdf.ffill().pct_change().to_numpy() < loss_limit
1074
1098
  ]
1075
1099
  .add(1)
1076
1100
  .to_numpy()
@@ -1079,8 +1103,9 @@ class OpenFrame(_CommonModel):
1079
1103
  downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
1080
1104
  )
1081
1105
  downidxarray = (
1082
- shortdf.pct_change()[
1083
- shortdf.pct_change().to_numpy() < loss_limit
1106
+ shortdf.ffill()
1107
+ .pct_change()[
1108
+ shortdf.ffill().pct_change().to_numpy() < loss_limit
1084
1109
  ]
1085
1110
  .add(1)
1086
1111
  .to_numpy()
@@ -1284,30 +1309,8 @@ class OpenFrame(_CommonModel):
1284
1309
 
1285
1310
  """
1286
1311
  full_year = 1.0
1287
- if all(
1288
- x == ValueType.RTRN
1289
- for x in self.tsdf.columns.get_level_values(1).to_numpy()
1290
- ):
1291
- msg = "asset should be a tuple[str, ValueType] or an integer."
1292
- if isinstance(asset, tuple):
1293
- asset_log = self.tsdf.loc[:, asset]
1294
- asset_cagr = asset_log.mean()
1295
- elif isinstance(asset, int):
1296
- asset_log = self.tsdf.iloc[:, asset]
1297
- asset_cagr = asset_log.mean()
1298
- else:
1299
- raise TypeError(msg)
1300
-
1301
- msg = "market should be a tuple[str, ValueType] or an integer."
1302
- if isinstance(market, tuple):
1303
- market_log = self.tsdf.loc[:, market]
1304
- market_cagr = market_log.mean()
1305
- elif isinstance(market, int):
1306
- market_log = self.tsdf.iloc[:, market]
1307
- market_cagr = market_log.mean()
1308
- else:
1309
- raise TypeError(msg)
1310
- else:
1312
+ vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
1313
+ if not any(vtypes):
1311
1314
  msg = "asset should be a tuple[str, ValueType] or an integer."
1312
1315
  if isinstance(asset, tuple):
1313
1316
  asset_log = log(
@@ -1375,6 +1378,29 @@ class OpenFrame(_CommonModel):
1375
1378
  )
1376
1379
  else:
1377
1380
  raise TypeError(msg)
1381
+ elif all(vtypes):
1382
+ msg = "asset should be a tuple[str, ValueType] or an integer."
1383
+ if isinstance(asset, tuple):
1384
+ asset_log = self.tsdf.loc[:, asset]
1385
+ asset_cagr = asset_log.mean()
1386
+ elif isinstance(asset, int):
1387
+ asset_log = self.tsdf.iloc[:, asset]
1388
+ asset_cagr = asset_log.mean()
1389
+ else:
1390
+ raise TypeError(msg)
1391
+
1392
+ msg = "market should be a tuple[str, ValueType] or an integer."
1393
+ if isinstance(market, tuple):
1394
+ market_log = self.tsdf.loc[:, market]
1395
+ market_cagr = market_log.mean()
1396
+ elif isinstance(market, int):
1397
+ market_log = self.tsdf.iloc[:, market]
1398
+ market_cagr = market_log.mean()
1399
+ else:
1400
+ raise TypeError(msg)
1401
+ else:
1402
+ msg = "Mix of series types will give inconsistent results"
1403
+ raise ValueError(msg)
1378
1404
 
1379
1405
  covariance = cov(asset_log, market_log, ddof=dlta_degr_freedms)
1380
1406
  beta = covariance[0, 1] / covariance[1, 1]
@@ -1407,27 +1433,30 @@ class OpenFrame(_CommonModel):
1407
1433
  "to run the make_portfolio method."
1408
1434
  )
1409
1435
  raise ValueError(msg)
1410
- dframe = self.tsdf.copy()
1411
- if not any(
1412
- x == ValueType.RTRN
1413
- for x in self.tsdf.columns.get_level_values(1).to_numpy()
1414
- ):
1415
- dframe = dframe.pct_change()
1416
- dframe.iloc[0] = 0
1436
+
1437
+ vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
1438
+ if not any(vtypes):
1439
+ returns = self.tsdf.ffill().pct_change()
1440
+ returns.iloc[0] = 0
1441
+ elif all(vtypes):
1442
+ returns = self.tsdf.copy()
1443
+ else:
1444
+ msg = "Mix of series types will give inconsistent results"
1445
+ raise ValueError(msg)
1417
1446
 
1418
1447
  msg = "Weight strategy not implemented"
1419
1448
  if weight_strat:
1420
1449
  if weight_strat == "eq_weights":
1421
1450
  self.weights = [1.0 / self.item_count] * self.item_count
1422
1451
  elif weight_strat == "inv_vol":
1423
- vol = divide(1.0, std(dframe, axis=0, ddof=1))
1452
+ vol = divide(1.0, std(returns, axis=0, ddof=1))
1424
1453
  vol[isinf(vol)] = nan
1425
1454
  self.weights = list(divide(vol, vol.sum()))
1426
1455
  else:
1427
1456
  raise NotImplementedError(msg)
1428
1457
 
1429
1458
  return DataFrame(
1430
- data=(dframe @ self.weights).add(1.0).cumprod(),
1459
+ data=(returns @ self.weights).add(1.0).cumprod(),
1431
1460
  index=self.tsdf.index,
1432
1461
  columns=[[name], [ValueType.PRICE]],
1433
1462
  dtype="float64",
@@ -1482,11 +1511,14 @@ class OpenFrame(_CommonModel):
1482
1511
  )
1483
1512
 
1484
1513
  retseries = (
1485
- relative.pct_change().rolling(observations, min_periods=observations).sum()
1514
+ relative.ffill()
1515
+ .pct_change()
1516
+ .rolling(observations, min_periods=observations)
1517
+ .sum()
1486
1518
  )
1487
1519
  retdf = retseries.dropna().to_frame()
1488
1520
 
1489
- voldf = relative.pct_change().rolling(
1521
+ voldf = relative.ffill().pct_change().rolling(
1490
1522
  observations,
1491
1523
  min_periods=observations,
1492
1524
  ).std() * sqrt(time_factor)
@@ -1530,9 +1562,13 @@ class OpenFrame(_CommonModel):
1530
1562
  asset_label = cast(tuple[str, str], self.tsdf.iloc[:, asset_column].name)[0]
1531
1563
  beta_label = f"{asset_label} / {market_label}"
1532
1564
 
1533
- rolling = self.tsdf.pct_change().rolling(
1534
- observations,
1535
- min_periods=observations,
1565
+ rolling = (
1566
+ self.tsdf.ffill()
1567
+ .pct_change()
1568
+ .rolling(
1569
+ observations,
1570
+ min_periods=observations,
1571
+ )
1536
1572
  )
1537
1573
 
1538
1574
  rcov = rolling.cov(ddof=dlta_degr_freedms)
@@ -1587,10 +1623,11 @@ class OpenFrame(_CommonModel):
1587
1623
  )
1588
1624
  first_series = (
1589
1625
  self.tsdf.iloc[:, first_column]
1626
+ .ffill()
1590
1627
  .pct_change()[1:]
1591
1628
  .rolling(observations, min_periods=observations)
1592
1629
  )
1593
- second_series = self.tsdf.iloc[:, second_column].pct_change()[1:]
1630
+ second_series = self.tsdf.iloc[:, second_column].ffill().pct_change()[1:]
1594
1631
  corrdf = first_series.corr(other=second_series).dropna().to_frame()
1595
1632
  corrdf.columns = MultiIndex.from_arrays(
1596
1633
  [
@@ -83,13 +83,15 @@ def simulate_portfolios(
83
83
  """
84
84
  copi = simframe.from_deepcopy()
85
85
 
86
- if any(
87
- x == ValueType.PRICE for x in copi.tsdf.columns.get_level_values(1).to_numpy()
88
- ):
86
+ vtypes = [x == ValueType.RTRN for x in copi.tsdf.columns.get_level_values(1)]
87
+ if not any(vtypes):
89
88
  copi.value_to_ret()
90
89
  log_ret = copi.tsdf.copy()[1:]
91
- else:
90
+ elif all(vtypes):
92
91
  log_ret = copi.tsdf.copy()
92
+ else:
93
+ msg = "Mix of series types will give inconsistent results"
94
+ raise ValueError(msg)
93
95
 
94
96
  log_ret.columns = log_ret.columns.droplevel(level=1)
95
97
 
@@ -165,13 +167,15 @@ def efficient_frontier( # noqa: C901
165
167
 
166
168
  copi = eframe.from_deepcopy()
167
169
 
168
- if any(
169
- x == ValueType.PRICE for x in copi.tsdf.columns.get_level_values(1).to_numpy()
170
- ):
170
+ vtypes = [x == ValueType.RTRN for x in copi.tsdf.columns.get_level_values(1)]
171
+ if not any(vtypes):
171
172
  copi.value_to_ret()
172
173
  log_ret = copi.tsdf.copy()[1:]
173
- else:
174
+ elif all(vtypes):
174
175
  log_ret = copi.tsdf.copy()
176
+ else:
177
+ msg = "Mix of series types will give inconsistent results"
178
+ raise ValueError(msg)
175
179
 
176
180
  log_ret.columns = log_ret.columns.droplevel(level=1)
177
181
 
@@ -304,7 +308,7 @@ def efficient_frontier( # noqa: C901
304
308
 
305
309
  if tweak:
306
310
  limit_tweak = 0.001
307
- line_df["stdev_diff"] = line_df.stdev.pct_change()
311
+ line_df["stdev_diff"] = line_df.stdev.ffill().pct_change()
308
312
  line_df = line_df.loc[line_df.stdev_diff.abs() > limit_tweak]
309
313
  line_df = line_df.drop(columns="stdev_diff")
310
314
 
@@ -346,6 +346,7 @@ class OpenTimeSeries(_CommonModel):
346
346
  - cast(DatetimeIndex, d_range)[:-1]
347
347
  ],
348
348
  )
349
+ # noinspection PyTypeChecker
349
350
  arr = list(cumprod(insert(1 + deltas * rate / 365, 0, 1.0)))
350
351
  dates = [d.strftime("%Y-%m-%d") for d in cast(DatetimeIndex, d_range)]
351
352
 
@@ -434,15 +435,12 @@ class OpenTimeSeries(_CommonModel):
434
435
  The returns of the values in the series
435
436
 
436
437
  """
437
- self.tsdf = self.tsdf.pct_change()
438
- self.tsdf.iloc[0] = 0
438
+ returns = self.tsdf.ffill().pct_change()
439
+ returns.iloc[0] = 0
439
440
  self.valuetype = ValueType.RTRN
440
- self.tsdf.columns = MultiIndex.from_arrays(
441
- [
442
- [self.label],
443
- [self.valuetype],
444
- ],
445
- )
441
+ arrays = [[self.label], [self.valuetype]]
442
+ returns.columns = MultiIndex.from_arrays(arrays=arrays)
443
+ self.tsdf = returns.copy()
446
444
  return self
447
445
 
448
446
  def value_to_diff(self: Self, periods: int = 1) -> Self:
@@ -480,14 +478,12 @@ class OpenTimeSeries(_CommonModel):
480
478
  An OpenTimeSeries object
481
479
 
482
480
  """
483
- if not any(
484
- x == ValueType.RTRN
485
- for x in cast(MultiIndex, self.tsdf.columns).get_level_values(1).to_numpy()
486
- ):
481
+ if self.valuetype == ValueType.PRICE:
487
482
  self.value_to_ret()
488
483
 
489
484
  self.tsdf = self.tsdf.add(1.0)
490
485
  self.tsdf = self.tsdf.cumprod(axis=0) / self.tsdf.iloc[0]
486
+
491
487
  self.valuetype = ValueType.PRICE
492
488
  self.tsdf.columns = MultiIndex.from_arrays(
493
489
  [
@@ -520,6 +516,7 @@ class OpenTimeSeries(_CommonModel):
520
516
  arr = array(self.values) / divider
521
517
 
522
518
  deltas = array([i.days for i in self.tsdf.index[1:] - self.tsdf.index[:-1]])
519
+ # noinspection PyTypeChecker
523
520
  arr = cumprod(insert(1.0 + deltas * arr[:-1] / days_in_year, 0, 1.0))
524
521
 
525
522
  self.dates = [d.strftime("%Y-%m-%d") for d in self.tsdf.index]
@@ -684,17 +681,13 @@ class OpenTimeSeries(_CommonModel):
684
681
  An OpenTimeSeries object
685
682
 
686
683
  """
687
- values: list[float]
688
- if any(
689
- x == ValueType.RTRN
690
- for x in cast(MultiIndex, self.tsdf.columns).get_level_values(1).to_numpy()
691
- ):
684
+ if self.valuetype == ValueType.RTRN:
692
685
  ra_df = self.tsdf.copy()
693
686
  values = [1.0]
694
687
  returns_input = True
695
688
  else:
696
689
  values = [cast(float, self.tsdf.iloc[0, 0])]
697
- ra_df = self.tsdf.pct_change()
690
+ ra_df = self.tsdf.ffill().pct_change()
698
691
  returns_input = False
699
692
  ra_df = ra_df.dropna()
700
693
 
@@ -818,6 +811,7 @@ def timeseries_chain(
818
811
 
819
812
  dates.extend([x.strftime("%Y-%m-%d") for x in new.tsdf.index])
820
813
 
814
+ # noinspection PyUnresolvedReferences
821
815
  if back.__class__.__subclasscheck__(
822
816
  OpenTimeSeries,
823
817
  ):
@@ -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.3"
3
+ version = "1.7.5"
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,7 +34,7 @@ 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"
@@ -43,7 +43,7 @@ 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"
@@ -51,9 +51,9 @@ coverage = ">=7.6.0,<8.0.0"
51
51
  genbadge = {version = ">=1.1.1,<2.0.0", extras = ["coverage"]}
52
52
  mypy = "^1.11.2"
53
53
  pandas-stubs = ">=2.1.2,<3.0.0"
54
- pre-commit = ">=3.7.1,<4.0.0"
54
+ pre-commit = ">=3.7.1,<6.0.0"
55
55
  pytest = ">=8.2.2,<9.0.0"
56
- ruff = "^0.6.5"
56
+ ruff = "^0.6.9"
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"
@@ -110,7 +110,7 @@ fixable = ["ALL"]
110
110
 
111
111
  [tool.ruff.lint.pylint]
112
112
  max-args = 12
113
- max-branches = 22
113
+ max-branches = 23
114
114
  max-statements = 59
115
115
 
116
116
  [tool.pytest.ini_options]
File without changes
File without changes