openseries 1.9.3__tar.gz → 1.9.4__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.3
2
2
  Name: openseries
3
- Version: 1.9.3
3
+ Version: 1.9.4
4
4
  Summary: Tools for analyzing financial timeseries.
5
5
  License: # BSD 3-Clause License
6
6
 
@@ -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
@@ -432,7 +433,17 @@ class _CommonModel(BaseModel): # type: ignore[misc]
432
433
 
433
434
  wmdf = wmdf.reindex(index=[deyt.date() for deyt in dates], method=method)
434
435
  wmdf.index = DatetimeIndex(wmdf.index)
435
- result = wmdf.pct_change().min()
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
+
446
+ result = wmdf.ffill().pct_change().min()
436
447
 
437
448
  if self.tsdf.shape[1] == 1:
438
449
  return float(result.iloc[0])
@@ -1305,6 +1316,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
1305
1316
 
1306
1317
  result = (
1307
1318
  self.tsdf.loc[cast("int", earlier) : cast("int", later)]
1319
+ .ffill()
1308
1320
  .pct_change()
1309
1321
  .mean()
1310
1322
  * time_factor
@@ -1366,7 +1378,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
1366
1378
  time_factor = how_many / fraction
1367
1379
 
1368
1380
  data = self.tsdf.loc[cast("int", earlier) : cast("int", later)]
1369
- result = data.pct_change().std().mul(sqrt(time_factor))
1381
+ result = data.ffill().pct_change().std().mul(sqrt(time_factor))
1370
1382
 
1371
1383
  if self.tsdf.shape[1] == 1:
1372
1384
  return float(cast("SupportsFloat", result.iloc[0]))
@@ -1564,21 +1576,24 @@ class _CommonModel(BaseModel): # type: ignore[misc]
1564
1576
  if drift_adjust:
1565
1577
  imp_vol = (-sqrt(time_factor) / norm.ppf(level)) * (
1566
1578
  self.tsdf.loc[cast("int", earlier) : cast("int", later)]
1579
+ .ffill()
1567
1580
  .pct_change()
1568
1581
  .quantile(1 - level, interpolation=interpolation)
1569
1582
  - self.tsdf.loc[cast("int", earlier) : cast("int", later)]
1583
+ .ffill()
1570
1584
  .pct_change()
1571
1585
  .sum()
1572
1586
  / len(
1573
- self.tsdf.loc[
1574
- cast("int", earlier) : cast("int", later)
1575
- ].pct_change(),
1587
+ self.tsdf.loc[cast("int", earlier) : cast("int", later)]
1588
+ .ffill()
1589
+ .pct_change(),
1576
1590
  )
1577
1591
  )
1578
1592
  else:
1579
1593
  imp_vol = (
1580
1594
  -sqrt(time_factor)
1581
1595
  * self.tsdf.loc[cast("int", earlier) : cast("int", later)]
1596
+ .ffill()
1582
1597
  .pct_change()
1583
1598
  .quantile(1 - level, interpolation=interpolation)
1584
1599
  / norm.ppf(level)
@@ -1644,12 +1659,14 @@ class _CommonModel(BaseModel): # type: ignore[misc]
1644
1659
  )
1645
1660
  result = [
1646
1661
  cvar_df.loc[:, x] # type: ignore[call-overload,index]
1662
+ .ffill()
1647
1663
  .pct_change()
1648
1664
  .sort_values()
1649
1665
  .iloc[
1650
1666
  : ceil(
1651
1667
  (1 - level)
1652
1668
  * cvar_df.loc[:, x] # type: ignore[index]
1669
+ .ffill()
1653
1670
  .pct_change()
1654
1671
  .count(),
1655
1672
  ),
@@ -1718,6 +1735,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
1718
1735
 
1719
1736
  how_many = (
1720
1737
  self.tsdf.loc[cast("int", earlier) : cast("int", later)]
1738
+ .ffill()
1721
1739
  .pct_change()
1722
1740
  .count(numeric_only=True)
1723
1741
  )
@@ -1734,6 +1752,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
1734
1752
  per_period_mar = min_accepted_return / time_factor
1735
1753
  diff = (
1736
1754
  self.tsdf.loc[cast("int", earlier) : cast("int", later)]
1755
+ .ffill()
1737
1756
  .pct_change()
1738
1757
  .sub(per_period_mar)
1739
1758
  )
@@ -1845,6 +1864,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
1845
1864
  )
1846
1865
  result: NDArray[float64] = skew(
1847
1866
  a=self.tsdf.loc[cast("int", earlier) : cast("int", later)]
1867
+ .ffill()
1848
1868
  .pct_change()
1849
1869
  .to_numpy(),
1850
1870
  bias=True,
@@ -1892,7 +1912,11 @@ class _CommonModel(BaseModel): # type: ignore[misc]
1892
1912
  to_dt=to_date,
1893
1913
  )
1894
1914
  result: NDArray[float64] = kurtosis(
1895
- a=(self.tsdf.loc[cast("int", earlier) : cast("int", later)].pct_change()),
1915
+ a=(
1916
+ self.tsdf.loc[cast("int", earlier) : cast("int", later)]
1917
+ .ffill()
1918
+ .pct_change()
1919
+ ),
1896
1920
  fisher=True,
1897
1921
  bias=True,
1898
1922
  nan_policy="omit",
@@ -1988,16 +2012,18 @@ class _CommonModel(BaseModel): # type: ignore[misc]
1988
2012
  )
1989
2013
  pos = (
1990
2014
  self.tsdf.loc[cast("int", earlier) : cast("int", later)]
2015
+ .ffill()
1991
2016
  .pct_change()[1:][
1992
- self.tsdf.loc[cast("int", earlier) : cast("int", later)].pct_change()[
1993
- 1:
1994
- ]
2017
+ self.tsdf.loc[cast("int", earlier) : cast("int", later)]
2018
+ .ffill()
2019
+ .pct_change()[1:]
1995
2020
  > zero
1996
2021
  ]
1997
2022
  .count()
1998
2023
  )
1999
2024
  tot = (
2000
2025
  self.tsdf.loc[cast("int", earlier) : cast("int", later)]
2026
+ .ffill()
2001
2027
  .pct_change()
2002
2028
  .count()
2003
2029
  )
@@ -2184,7 +2210,11 @@ class _CommonModel(BaseModel): # type: ignore[misc]
2184
2210
  from_dt=from_date,
2185
2211
  to_dt=to_date,
2186
2212
  )
2187
- retdf = self.tsdf.loc[cast("int", earlier) : cast("int", later)].pct_change()
2213
+ retdf = (
2214
+ self.tsdf.loc[cast("int", earlier) : cast("int", later)]
2215
+ .ffill()
2216
+ .pct_change()
2217
+ )
2188
2218
  pos = retdf[retdf > min_accepted_return].sub(min_accepted_return).sum()
2189
2219
  neg = retdf[retdf < min_accepted_return].sub(min_accepted_return).sum()
2190
2220
  ratio = pos / -neg
@@ -2272,7 +2302,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
2272
2302
  period = "-".join([str(year), str(month).zfill(2)])
2273
2303
  vrdf = self.tsdf.copy()
2274
2304
  vrdf.index = DatetimeIndex(vrdf.index)
2275
- resultdf = DataFrame(vrdf.pct_change())
2305
+ resultdf = DataFrame(vrdf.ffill().pct_change())
2276
2306
  result = resultdf.loc[period] + 1
2277
2307
  cal_period = result.cumprod(axis="index").iloc[-1] - 1
2278
2308
  if self.tsdf.shape[1] == 1:
@@ -2324,6 +2354,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
2324
2354
  )
2325
2355
  result = (
2326
2356
  self.tsdf.loc[cast("int", earlier) : cast("int", later)]
2357
+ .ffill()
2327
2358
  .pct_change()
2328
2359
  .quantile(1 - level, interpolation=interpolation)
2329
2360
  )
@@ -2372,6 +2403,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
2372
2403
  )
2373
2404
  result = (
2374
2405
  self.tsdf.loc[cast("int", earlier) : cast("int", later)]
2406
+ .ffill()
2375
2407
  .pct_change()
2376
2408
  .rolling(observations, min_periods=observations)
2377
2409
  .sum()
@@ -2418,9 +2450,11 @@ class _CommonModel(BaseModel): # type: ignore[misc]
2418
2450
  from_dt=from_date,
2419
2451
  to_dt=to_date,
2420
2452
  )
2421
- zscframe = self.tsdf.loc[
2422
- cast("int", earlier) : cast("int", later)
2423
- ].pct_change()
2453
+ zscframe = (
2454
+ self.tsdf.loc[cast("int", earlier) : cast("int", later)]
2455
+ .ffill()
2456
+ .pct_change()
2457
+ )
2424
2458
  result = (zscframe.iloc[-1] - zscframe.mean()) / zscframe.std()
2425
2459
 
2426
2460
  if self.tsdf.shape[1] == 1:
@@ -2489,6 +2523,7 @@ class _CommonModel(BaseModel): # type: ignore[misc]
2489
2523
  ret_label = cast("tuple[str]", self.tsdf.iloc[:, column].name)[0]
2490
2524
  retseries = (
2491
2525
  Series(self.tsdf.iloc[:, column])
2526
+ .ffill()
2492
2527
  .pct_change()
2493
2528
  .rolling(observations, min_periods=observations)
2494
2529
  .sum()
@@ -57,6 +57,7 @@ from .owntypes import (
57
57
  NoWeightsError,
58
58
  OpenFramePropertiesList,
59
59
  RatioInputError,
60
+ ResampleDataLossError,
60
61
  Self,
61
62
  ValueType,
62
63
  )
@@ -340,7 +341,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
340
341
  The returns of the values in the series
341
342
 
342
343
  """
343
- returns = self.tsdf.pct_change()
344
+ returns = self.tsdf.ffill().pct_change()
344
345
  returns.iloc[0] = 0
345
346
  new_labels: list[ValueType] = [ValueType.RTRN] * self.item_count
346
347
  arrays: list[Index[Any], list[ValueType]] = [ # type: ignore[type-arg]
@@ -387,7 +388,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
387
388
  """
388
389
  vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
389
390
  if not any(vtypes):
390
- returns = self.tsdf.pct_change()
391
+ returns = self.tsdf.ffill().pct_change()
391
392
  returns.iloc[0] = 0
392
393
  elif all(vtypes):
393
394
  returns = self.tsdf.copy()
@@ -424,12 +425,27 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
424
425
  An OpenFrame object
425
426
 
426
427
  """
428
+ vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
429
+ if not any(vtypes):
430
+ value_type = ValueType.PRICE
431
+ elif all(vtypes):
432
+ value_type = ValueType.RTRN
433
+ else:
434
+ msg = "Mix of series types will give inconsistent results"
435
+ raise MixedValuetypesError(msg)
436
+
427
437
  self.tsdf.index = DatetimeIndex(self.tsdf.index)
428
- self.tsdf = self.tsdf.resample(freq).last()
438
+ if value_type == ValueType.PRICE:
439
+ self.tsdf = self.tsdf.resample(freq).last()
440
+ else:
441
+ self.tsdf = self.tsdf.resample(freq).sum()
429
442
  self.tsdf.index = Index(d.date() for d in DatetimeIndex(self.tsdf.index))
430
443
  for xerie in self.constituents:
431
444
  xerie.tsdf.index = DatetimeIndex(xerie.tsdf.index)
432
- xerie.tsdf = xerie.tsdf.resample(freq).last()
445
+ if value_type == ValueType.PRICE:
446
+ xerie.tsdf = xerie.tsdf.resample(freq).last()
447
+ else:
448
+ xerie.tsdf = xerie.tsdf.resample(freq).sum()
433
449
  xerie.tsdf.index = Index(
434
450
  dejt.date() for dejt in DatetimeIndex(xerie.tsdf.index)
435
451
  )
@@ -458,6 +474,15 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
458
474
  An OpenFrame object
459
475
 
460
476
  """
477
+ vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
478
+ if any(vtypes):
479
+ msg = (
480
+ "Do not run resample_to_business_period_ends on return series. "
481
+ "The operation will pick the last data point in the sparser series. "
482
+ "It will not sum returns and therefore data will be lost."
483
+ )
484
+ raise ResampleDataLossError(msg)
485
+
461
486
  for xerie in self.constituents:
462
487
  dates = _do_resample_to_business_period_ends(
463
488
  data=xerie.tsdf,
@@ -530,7 +555,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
530
555
  Series volatilities and correlation
531
556
 
532
557
  """
533
- earlier, later = self.calc_range(months_from_last, from_date, to_date)
558
+ earlier, later = self.calc_range(
559
+ months_offset=months_from_last, from_dt=from_date, to_dt=to_date
560
+ )
534
561
  if periods_in_a_year_fixed is None:
535
562
  fraction = (later - earlier).days / 365.25
536
563
  how_many = (
@@ -621,9 +648,13 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
621
648
  Correlation matrix
622
649
 
623
650
  """
624
- corr_matrix = self.tsdf.pct_change().corr(
625
- method="pearson",
626
- min_periods=1,
651
+ corr_matrix = (
652
+ self.tsdf.ffill()
653
+ .pct_change()
654
+ .corr(
655
+ method="pearson",
656
+ min_periods=1,
657
+ )
627
658
  )
628
659
  corr_matrix.columns = corr_matrix.columns.droplevel(level=1)
629
660
  corr_matrix.index = corr_matrix.index.droplevel(level=1)
@@ -805,7 +836,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
805
836
  Tracking Errors
806
837
 
807
838
  """
808
- earlier, later = self.calc_range(months_from_last, from_date, to_date)
839
+ earlier, later = self.calc_range(
840
+ months_offset=months_from_last, from_dt=from_date, to_dt=to_date
841
+ )
809
842
  fraction = (later - earlier).days / 365.25
810
843
 
811
844
  msg = "base_column should be a tuple[str, ValueType] or an integer."
@@ -848,10 +881,8 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
848
881
  :,
849
882
  item,
850
883
  ]
851
- relative = 1.0 + longdf - shortdf
852
- vol = float(
853
- relative.pct_change().std() * sqrt(time_factor),
854
- )
884
+ relative = longdf.ffill().pct_change() - shortdf.ffill().pct_change()
885
+ vol = float(relative.std() * sqrt(time_factor))
855
886
  terrors.append(vol)
856
887
 
857
888
  return Series(
@@ -897,7 +928,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
897
928
  Information Ratios
898
929
 
899
930
  """
900
- earlier, later = self.calc_range(months_from_last, from_date, to_date)
931
+ earlier, later = self.calc_range(
932
+ months_offset=months_from_last, from_dt=from_date, to_dt=to_date
933
+ )
901
934
  fraction = (later - earlier).days / 365.25
902
935
 
903
936
  msg = "base_column should be a tuple[str, ValueType] or an integer."
@@ -940,13 +973,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
940
973
  :,
941
974
  item,
942
975
  ]
943
- relative = 1.0 + longdf - shortdf
944
- ret = float(
945
- relative.pct_change().mean() * time_factor,
946
- )
947
- vol = float(
948
- relative.pct_change().std() * sqrt(time_factor),
949
- )
976
+ relative = longdf.ffill().pct_change() - shortdf.ffill().pct_change()
977
+ ret = float(relative.mean() * time_factor)
978
+ vol = float(relative.std() * sqrt(time_factor))
950
979
  ratios.append(ret / vol)
951
980
 
952
981
  return Series(
@@ -1000,7 +1029,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1000
1029
 
1001
1030
  """
1002
1031
  loss_limit: float = 0.0
1003
- earlier, later = self.calc_range(months_from_last, from_date, to_date)
1032
+ earlier, later = self.calc_range(
1033
+ months_offset=months_from_last, from_dt=from_date, to_dt=to_date
1034
+ )
1004
1035
  fraction = (later - earlier).days / 365.25
1005
1036
 
1006
1037
  msg = "base_column should be a tuple[str, ValueType] or an integer."
@@ -1046,16 +1077,18 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1046
1077
  msg = "ratio must be one of 'up', 'down' or 'both'."
1047
1078
  if ratio == "up":
1048
1079
  uparray = (
1049
- longdf.pct_change()[
1050
- shortdf.pct_change().to_numpy() > loss_limit
1080
+ longdf.ffill()
1081
+ .pct_change()[
1082
+ shortdf.ffill().pct_change().to_numpy() > loss_limit
1051
1083
  ]
1052
1084
  .add(1)
1053
1085
  .to_numpy()
1054
1086
  )
1055
1087
  up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
1056
1088
  upidxarray = (
1057
- shortdf.pct_change()[
1058
- shortdf.pct_change().to_numpy() > loss_limit
1089
+ shortdf.ffill()
1090
+ .pct_change()[
1091
+ shortdf.ffill().pct_change().to_numpy() > loss_limit
1059
1092
  ]
1060
1093
  .add(1)
1061
1094
  .to_numpy()
@@ -1066,8 +1099,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1066
1099
  ratios.append(up_rtrn / up_idx_return)
1067
1100
  elif ratio == "down":
1068
1101
  downarray = (
1069
- longdf.pct_change()[
1070
- shortdf.pct_change().to_numpy() < loss_limit
1102
+ longdf.ffill()
1103
+ .pct_change()[
1104
+ shortdf.ffill().pct_change().to_numpy() < loss_limit
1071
1105
  ]
1072
1106
  .add(1)
1073
1107
  .to_numpy()
@@ -1076,8 +1110,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1076
1110
  downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
1077
1111
  )
1078
1112
  downidxarray = (
1079
- shortdf.pct_change()[
1080
- shortdf.pct_change().to_numpy() < loss_limit
1113
+ shortdf.ffill()
1114
+ .pct_change()[
1115
+ shortdf.ffill().pct_change().to_numpy() < loss_limit
1081
1116
  ]
1082
1117
  .add(1)
1083
1118
  .to_numpy()
@@ -1089,16 +1124,18 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1089
1124
  ratios.append(down_return / down_idx_return)
1090
1125
  elif ratio == "both":
1091
1126
  uparray = (
1092
- longdf.pct_change()[
1093
- shortdf.pct_change().to_numpy() > loss_limit
1127
+ longdf.ffill()
1128
+ .pct_change()[
1129
+ shortdf.ffill().pct_change().to_numpy() > loss_limit
1094
1130
  ]
1095
1131
  .add(1)
1096
1132
  .to_numpy()
1097
1133
  )
1098
1134
  up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
1099
1135
  upidxarray = (
1100
- shortdf.pct_change()[
1101
- shortdf.pct_change().to_numpy() > loss_limit
1136
+ shortdf.ffill()
1137
+ .pct_change()[
1138
+ shortdf.ffill().pct_change().to_numpy() > loss_limit
1102
1139
  ]
1103
1140
  .add(1)
1104
1141
  .to_numpy()
@@ -1107,8 +1144,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1107
1144
  upidxarray.prod() ** (1 / (len(upidxarray) / time_factor)) - 1
1108
1145
  )
1109
1146
  downarray = (
1110
- longdf.pct_change()[
1111
- shortdf.pct_change().to_numpy() < loss_limit
1147
+ longdf.ffill()
1148
+ .pct_change()[
1149
+ shortdf.ffill().pct_change().to_numpy() < loss_limit
1112
1150
  ]
1113
1151
  .add(1)
1114
1152
  .to_numpy()
@@ -1117,8 +1155,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1117
1155
  downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
1118
1156
  )
1119
1157
  downidxarray = (
1120
- shortdf.pct_change()[
1121
- shortdf.pct_change().to_numpy() < loss_limit
1158
+ shortdf.ffill()
1159
+ .pct_change()[
1160
+ shortdf.ffill().pct_change().to_numpy() < loss_limit
1122
1161
  ]
1123
1162
  .add(1)
1124
1163
  .to_numpy()
@@ -1173,10 +1212,8 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1173
1212
  Beta as Co-variance of x & y divided by Variance of x
1174
1213
 
1175
1214
  """
1176
- if all(
1177
- x_value == ValueType.RTRN
1178
- for x_value in self.tsdf.columns.get_level_values(1).to_numpy()
1179
- ):
1215
+ vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
1216
+ if all(vtypes):
1180
1217
  msg = "asset should be a tuple[str, ValueType] or an integer."
1181
1218
  if isinstance(asset, tuple):
1182
1219
  y_value = self.tsdf.loc[:, asset]
@@ -1192,33 +1229,27 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1192
1229
  x_value = self.tsdf.iloc[:, market]
1193
1230
  else:
1194
1231
  raise TypeError(msg)
1195
- else:
1232
+ elif not any(vtypes):
1196
1233
  msg = "asset should be a tuple[str, ValueType] or an integer."
1197
1234
  if isinstance(asset, tuple):
1198
- y_value = log(
1199
- self.tsdf.loc[:, asset] / self.tsdf.loc[:, asset].iloc[0],
1200
- )
1235
+ y_value = self.tsdf.loc[:, asset].ffill().pct_change().iloc[1:]
1201
1236
  elif isinstance(asset, int):
1202
- y_value = log(
1203
- self.tsdf.iloc[:, asset] / cast("float", self.tsdf.iloc[0, asset]),
1204
- )
1237
+ y_value = self.tsdf.iloc[:, asset].ffill().pct_change().iloc[1:]
1205
1238
  else:
1206
1239
  raise TypeError(msg)
1207
-
1208
1240
  msg = "market should be a tuple[str, ValueType] or an integer."
1241
+
1209
1242
  if isinstance(market, tuple):
1210
- x_value = log(
1211
- self.tsdf.loc[:, market] / self.tsdf.loc[:, market].iloc[0],
1212
- )
1243
+ x_value = self.tsdf.loc[:, market].ffill().pct_change().iloc[1:]
1213
1244
  elif isinstance(market, int):
1214
- x_value = log(
1215
- self.tsdf.iloc[:, market]
1216
- / cast("float", self.tsdf.iloc[0, market]),
1217
- )
1245
+ x_value = self.tsdf.iloc[:, market].ffill().pct_change().iloc[1:]
1218
1246
  else:
1219
1247
  raise TypeError(msg)
1248
+ else:
1249
+ msg = "Mix of series types will give inconsistent results"
1250
+ raise MixedValuetypesError(msg)
1220
1251
 
1221
- covariance = cov(y_value, x_value, ddof=dlta_degr_freedms)
1252
+ covariance = cov(m=y_value, y=x_value, ddof=dlta_degr_freedms)
1222
1253
  beta = covariance[0, 1] / covariance[1, 1]
1223
1254
 
1224
1255
  return float(beta)
@@ -1319,105 +1350,57 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1319
1350
  Jensen's alpha
1320
1351
 
1321
1352
  """
1322
- full_year = 1.0
1323
1353
  vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
1324
1354
  if not any(vtypes):
1325
1355
  msg = "asset should be a tuple[str, ValueType] or an integer."
1326
1356
  if isinstance(asset, tuple):
1327
- asset_log = log(
1328
- self.tsdf.loc[:, asset] / self.tsdf.loc[:, asset].iloc[0],
1329
- )
1330
- if self.yearfrac > full_year:
1331
- asset_cagr = (
1332
- self.tsdf.loc[:, asset].iloc[-1]
1333
- / self.tsdf.loc[:, asset].iloc[0]
1334
- ) ** (1 / self.yearfrac) - 1
1335
- else:
1336
- asset_cagr = (
1337
- self.tsdf.loc[:, asset].iloc[-1]
1338
- / self.tsdf.loc[:, asset].iloc[0]
1339
- - 1
1340
- )
1357
+ asset_rtn = self.tsdf.loc[:, asset].ffill().pct_change().iloc[1:]
1358
+ asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
1341
1359
  elif isinstance(asset, int):
1342
- asset_log = log(
1343
- self.tsdf.iloc[:, asset] / cast("float", self.tsdf.iloc[0, asset]),
1344
- )
1345
- if self.yearfrac > full_year:
1346
- asset_cagr = (
1347
- cast("float", self.tsdf.iloc[-1, asset])
1348
- / cast("float", self.tsdf.iloc[0, asset])
1349
- ) ** (1 / self.yearfrac) - 1
1350
- else:
1351
- asset_cagr = (
1352
- cast("float", self.tsdf.iloc[-1, asset])
1353
- / cast("float", self.tsdf.iloc[0, asset])
1354
- - 1
1355
- )
1360
+ asset_rtn = self.tsdf.iloc[:, asset].ffill().pct_change().iloc[1:]
1361
+ asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
1356
1362
  else:
1357
1363
  raise TypeError(msg)
1358
1364
 
1359
1365
  msg = "market should be a tuple[str, ValueType] or an integer."
1360
1366
  if isinstance(market, tuple):
1361
- market_log = log(
1362
- self.tsdf.loc[:, market] / self.tsdf.loc[:, market].iloc[0],
1363
- )
1364
- if self.yearfrac > full_year:
1365
- market_cagr = (
1366
- self.tsdf.loc[:, market].iloc[-1]
1367
- / self.tsdf.loc[:, market].iloc[0]
1368
- ) ** (1 / self.yearfrac) - 1
1369
- else:
1370
- market_cagr = (
1371
- self.tsdf.loc[:, market].iloc[-1]
1372
- / self.tsdf.loc[:, market].iloc[0]
1373
- - 1
1374
- )
1367
+ market_rtn = self.tsdf.loc[:, market].ffill().pct_change().iloc[1:]
1368
+ market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
1375
1369
  elif isinstance(market, int):
1376
- market_log = log(
1377
- self.tsdf.iloc[:, market]
1378
- / cast("float", self.tsdf.iloc[0, market]),
1379
- )
1380
- if self.yearfrac > full_year:
1381
- market_cagr = (
1382
- cast("float", self.tsdf.iloc[-1, market])
1383
- / cast("float", self.tsdf.iloc[0, market])
1384
- ) ** (1 / self.yearfrac) - 1
1385
- else:
1386
- market_cagr = (
1387
- cast("float", self.tsdf.iloc[-1, market])
1388
- / cast("float", self.tsdf.iloc[0, market])
1389
- - 1
1390
- )
1370
+ market_rtn = self.tsdf.iloc[:, market].ffill().pct_change().iloc[1:]
1371
+ market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
1391
1372
  else:
1392
1373
  raise TypeError(msg)
1393
1374
  elif all(vtypes):
1394
1375
  msg = "asset should be a tuple[str, ValueType] or an integer."
1395
1376
  if isinstance(asset, tuple):
1396
- asset_log = self.tsdf.loc[:, asset]
1397
- asset_cagr = asset_log.mean()
1377
+ asset_rtn = self.tsdf.loc[:, asset]
1378
+ asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
1398
1379
  elif isinstance(asset, int):
1399
- asset_log = self.tsdf.iloc[:, asset]
1400
- asset_cagr = asset_log.mean()
1380
+ asset_rtn = self.tsdf.iloc[:, asset]
1381
+ asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
1401
1382
  else:
1402
1383
  raise TypeError(msg)
1403
1384
 
1404
1385
  msg = "market should be a tuple[str, ValueType] or an integer."
1405
1386
  if isinstance(market, tuple):
1406
- market_log = self.tsdf.loc[:, market]
1407
- market_cagr = market_log.mean()
1387
+ market_rtn = self.tsdf.loc[:, market]
1388
+ market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
1408
1389
  elif isinstance(market, int):
1409
- market_log = self.tsdf.iloc[:, market]
1410
- market_cagr = market_log.mean()
1390
+ market_rtn = self.tsdf.iloc[:, market]
1391
+ market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
1411
1392
  else:
1412
1393
  raise TypeError(msg)
1413
1394
  else:
1414
1395
  msg = "Mix of series types will give inconsistent results"
1415
1396
  raise MixedValuetypesError(msg)
1416
1397
 
1417
- covariance = cov(m=asset_log, y=market_log, ddof=dlta_degr_freedms)
1398
+ covariance = cov(m=asset_rtn, y=market_rtn, ddof=dlta_degr_freedms)
1418
1399
  beta = covariance[0, 1] / covariance[1, 1]
1419
1400
 
1420
- return float(asset_cagr - riskfree_rate - beta * (market_cagr - riskfree_rate))
1401
+ return float(
1402
+ asset_rtn_mean - riskfree_rate - beta * (market_rtn_mean - riskfree_rate)
1403
+ )
1421
1404
 
1422
1405
  def make_portfolio(
1423
1406
  self: Self,
@@ -1448,7 +1431,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1448
1431
 
1449
1432
  vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
1450
1433
  if not any(vtypes):
1451
- returns = self.tsdf.pct_change()
1434
+ returns = self.tsdf.ffill().pct_change()
1452
1435
  returns.iloc[0] = 0
1453
1436
  elif all(vtypes):
1454
1437
  returns = self.tsdf.copy()
@@ -1523,11 +1506,14 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1523
1506
  )
1524
1507
 
1525
1508
  retseries = (
1526
- relative.pct_change().rolling(observations, min_periods=observations).sum()
1509
+ relative.ffill()
1510
+ .pct_change()
1511
+ .rolling(observations, min_periods=observations)
1512
+ .sum()
1527
1513
  )
1528
1514
  retdf = retseries.dropna().to_frame()
1529
1515
 
1530
- voldf = relative.pct_change().rolling(
1516
+ voldf = relative.ffill().pct_change().rolling(
1531
1517
  observations,
1532
1518
  min_periods=observations,
1533
1519
  ).std() * sqrt(time_factor)
@@ -1573,9 +1559,13 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1573
1559
  asset_label = cast("tuple[str, str]", self.tsdf.iloc[:, asset_column].name)[0]
1574
1560
  beta_label = f"{asset_label} / {market_label}"
1575
1561
 
1576
- rolling = self.tsdf.pct_change().rolling(
1577
- observations,
1578
- min_periods=observations,
1562
+ rolling = (
1563
+ self.tsdf.ffill()
1564
+ .pct_change()
1565
+ .rolling(
1566
+ observations,
1567
+ min_periods=observations,
1568
+ )
1579
1569
  )
1580
1570
 
1581
1571
  rcov = rolling.cov(ddof=dlta_degr_freedms)
@@ -1630,10 +1620,11 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1630
1620
  )
1631
1621
  first_series = (
1632
1622
  self.tsdf.iloc[:, first_column]
1623
+ .ffill()
1633
1624
  .pct_change()[1:]
1634
1625
  .rolling(observations, min_periods=observations)
1635
1626
  )
1636
- second_series = self.tsdf.iloc[:, second_column].pct_change()[1:]
1627
+ second_series = self.tsdf.iloc[:, second_column].ffill().pct_change()[1:]
1637
1628
  corrdf = first_series.corr(other=second_series).dropna().to_frame()
1638
1629
  corrdf.columns = MultiIndex.from_arrays(
1639
1630
  [
@@ -376,3 +376,7 @@ class IncorrectArgumentComboError(Exception):
376
376
 
377
377
  class PropertiesInputValidationError(Exception):
378
378
  """Raised when duplicate strings are provided."""
379
+
380
+
381
+ class ResampleDataLossError(Exception):
382
+ """Raised when user attempts to run resample_to_business_period_ends on returns."""
@@ -320,7 +320,7 @@ def efficient_frontier(
320
320
 
321
321
  if tweak:
322
322
  limit_tweak = 0.001
323
- line_df["stdev_diff"] = line_df.stdev.pct_change()
323
+ line_df["stdev_diff"] = line_df.stdev.ffill().pct_change()
324
324
  line_df = line_df.loc[line_df.stdev_diff.abs() > limit_tweak]
325
325
  line_df = line_df.drop(columns="stdev_diff")
326
326
 
@@ -68,9 +68,7 @@ def calendar_period_returns(
68
68
  """
69
69
  copied = data.from_deepcopy()
70
70
  copied.resample_to_business_period_ends(freq=freq)
71
- vtypes = [x == ValueType.RTRN for x in copied.tsdf.columns.get_level_values(1)]
72
- if not any(vtypes):
73
- copied.value_to_ret()
71
+ copied.value_to_ret()
74
72
  cldr = copied.tsdf.iloc[1:].copy()
75
73
  if relabel:
76
74
  if freq.upper() == "BYE":
@@ -53,6 +53,7 @@ from .owntypes import (
53
53
  LiteralSeriesProps,
54
54
  MarketsNotStringNorListStrError,
55
55
  OpenTimeSeriesPropertiesList,
56
+ ResampleDataLossError,
56
57
  Self,
57
58
  ValueListType,
58
59
  ValueType,
@@ -66,7 +67,7 @@ TypeOpenTimeSeries = TypeVar("TypeOpenTimeSeries", bound="OpenTimeSeries")
66
67
 
67
68
 
68
69
  # noinspection PyUnresolvedReferences,PyNestedDecorators
69
- class OpenTimeSeries(_CommonModel):
70
+ class OpenTimeSeries(_CommonModel): # type: ignore[misc]
70
71
  """OpenTimeSeries objects are at the core of the openseries package.
71
72
 
72
73
  The intended use is to allow analyses of financial timeseries.
@@ -468,7 +469,7 @@ class OpenTimeSeries(_CommonModel):
468
469
  The returns of the values in the series
469
470
 
470
471
  """
471
- returns = self.tsdf.pct_change()
472
+ returns = self.tsdf.ffill().pct_change()
472
473
  returns.iloc[0] = 0
473
474
  self.valuetype = ValueType.RTRN
474
475
  arrays = [[self.label], [self.valuetype]]
@@ -586,7 +587,10 @@ class OpenTimeSeries(_CommonModel):
586
587
 
587
588
  """
588
589
  self.tsdf.index = DatetimeIndex(self.tsdf.index)
589
- self.tsdf = self.tsdf.resample(freq).last()
590
+ if self.valuetype == ValueType.RTRN:
591
+ self.tsdf = self.tsdf.resample(freq).sum()
592
+ else:
593
+ self.tsdf = self.tsdf.resample(freq).last()
590
594
  self.tsdf.index = Index(d.date() for d in DatetimeIndex(self.tsdf.index))
591
595
  return self
592
596
 
@@ -612,6 +616,14 @@ class OpenTimeSeries(_CommonModel):
612
616
  An OpenTimeSeries object
613
617
 
614
618
  """
619
+ if self.valuetype == ValueType.RTRN:
620
+ msg = (
621
+ "Do not run resample_to_business_period_ends on return series. "
622
+ "The operation will pick the last data point in the sparser series. "
623
+ "It will not sum returns and therefore data will be lost."
624
+ )
625
+ raise ResampleDataLossError(msg)
626
+
615
627
  dates = _do_resample_to_business_period_ends(
616
628
  data=self.tsdf,
617
629
  freq=freq,
@@ -659,7 +671,9 @@ class OpenTimeSeries(_CommonModel):
659
671
  Series EWMA volatility
660
672
 
661
673
  """
662
- earlier, later = self.calc_range(months_from_last, from_date, to_date)
674
+ earlier, later = self.calc_range(
675
+ months_offset=months_from_last, from_dt=from_date, to_dt=to_date
676
+ )
663
677
  if periods_in_a_year_fixed:
664
678
  time_factor = float(periods_in_a_year_fixed)
665
679
  else:
@@ -725,7 +739,7 @@ class OpenTimeSeries(_CommonModel):
725
739
  returns_input = True
726
740
  else:
727
741
  values = [cast("float", self.tsdf.iloc[0, 0])]
728
- ra_df = self.tsdf.pct_change()
742
+ ra_df = self.tsdf.ffill().pct_change()
729
743
  returns_input = False
730
744
  ra_df = ra_df.dropna()
731
745
 
@@ -129,7 +129,9 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
129
129
  """
130
130
  return cast(
131
131
  "float",
132
- (self.results.pct_change().mean() * self.trading_days_in_year).iloc[0],
132
+ (
133
+ self.results.ffill().pct_change().mean() * self.trading_days_in_year
134
+ ).iloc[0],
133
135
  )
134
136
 
135
137
  @property
@@ -144,9 +146,10 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
144
146
  """
145
147
  return cast(
146
148
  "float",
147
- (self.results.pct_change().std() * sqrt(self.trading_days_in_year)).iloc[
148
- 0
149
- ],
149
+ (
150
+ self.results.ffill().pct_change().std()
151
+ * sqrt(self.trading_days_in_year)
152
+ ).iloc[0],
150
153
  )
151
154
 
152
155
  @classmethod
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openseries"
3
- version = "1.9.3"
3
+ version = "1.9.4"
4
4
  description = "Tools for analyzing financial timeseries."
5
5
  authors = [
6
6
  { name = "Martin Karrin", email = "martin.karrin@captor.se" },
@@ -67,7 +67,7 @@ pre-commit = ">=3.7.1,<6.0.0"
67
67
  pytest = ">=8.2.2,<9.0.0"
68
68
  pytest-cov = ">=5.0.0,<7.0.0"
69
69
  pytest-xdist = ">=3.3.1,<5.0.0"
70
- ruff = "0.12.8"
70
+ ruff = "0.12.10"
71
71
  types-openpyxl = ">=3.1.2,<5.0.0"
72
72
  types-python-dateutil = ">=2.8.2,<4.0.0"
73
73
  types-requests = ">=2.20.0,<3.0.0"
File without changes
File without changes