openseries 1.7.2__py3-none-any.whl → 1.7.4__py3-none-any.whl

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.
@@ -18,7 +18,15 @@ if TYPE_CHECKING:
18
18
  from openpyxl.utils.dataframe import dataframe_to_rows
19
19
  from openpyxl.workbook.workbook import Workbook
20
20
  from openpyxl.worksheet.worksheet import Worksheet
21
- from pandas import DataFrame, DatetimeIndex, Index, MultiIndex, Series, date_range
21
+ from pandas import (
22
+ DataFrame,
23
+ DatetimeIndex,
24
+ Index,
25
+ MultiIndex,
26
+ Series,
27
+ date_range,
28
+ to_datetime,
29
+ )
22
30
  from pandas.tseries.offsets import CustomBusinessDay
23
31
  from plotly.graph_objs import Figure # type: ignore[import-untyped,unused-ignore]
24
32
  from plotly.io import to_html # type: ignore[import-untyped,unused-ignore]
@@ -51,6 +59,7 @@ from .types import (
51
59
  )
52
60
 
53
61
 
62
+ # noinspection PyTypeChecker
54
63
  class _CommonModel(BaseModel):
55
64
  """Declare _CommonModel."""
56
65
 
@@ -351,9 +360,7 @@ class _CommonModel(BaseModel):
351
360
  """
352
361
  wmdf = self.tsdf.copy()
353
362
  wmdf.index = DatetimeIndex(wmdf.index)
354
- result = (
355
- wmdf.resample("BME").last().pct_change(fill_method=cast(str, None)).min()
356
- )
363
+ result = wmdf.resample("BME").last().pct_change().min()
357
364
 
358
365
  if self.tsdf.shape[1] == 1:
359
366
  return float(result.iloc[0])
@@ -521,8 +528,8 @@ class _CommonModel(BaseModel):
521
528
  An OpenFrame object
522
529
 
523
530
  """
524
- startyear = DatetimeIndex(self.tsdf.index)[0].year
525
- endyear = DatetimeIndex(self.tsdf.index)[-1].year
531
+ startyear = to_datetime(self.tsdf.index[0]).year
532
+ endyear = to_datetime(self.tsdf.index[-1]).year
526
533
  calendar = holiday_calendar(
527
534
  startyear=startyear,
528
535
  endyear=endyear,
@@ -674,7 +681,7 @@ class _CommonModel(BaseModel):
674
681
  output.append(dict(itemdata))
675
682
 
676
683
  with dirpath.joinpath(filename).open(mode="w", encoding="utf-8") as jsonfile:
677
- dump(output, jsonfile, indent=2, sort_keys=False)
684
+ dump(obj=output, fp=jsonfile, indent=2, sort_keys=False)
678
685
 
679
686
  return output
680
687
 
@@ -1022,7 +1029,8 @@ class _CommonModel(BaseModel):
1022
1029
 
1023
1030
  result = (
1024
1031
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1025
- .pct_change(fill_method=cast(str, None))
1032
+ .ffill()
1033
+ .pct_change()
1026
1034
  .mean()
1027
1035
  * time_factor
1028
1036
  )
@@ -1081,9 +1089,7 @@ class _CommonModel(BaseModel):
1081
1089
  time_factor = how_many / fraction
1082
1090
 
1083
1091
  data = self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1084
- result = (
1085
- data.pct_change(fill_method=cast(str, None)).std().mul(sqrt(time_factor))
1086
- )
1092
+ result = data.ffill().pct_change().std().mul(sqrt(time_factor))
1087
1093
 
1088
1094
  if self.tsdf.shape[1] == 1:
1089
1095
  return float(cast(SupportsFloat, result.iloc[0]))
@@ -1279,22 +1285,20 @@ class _CommonModel(BaseModel):
1279
1285
  if drift_adjust:
1280
1286
  imp_vol = (-sqrt(time_factor) / norm.ppf(level)) * (
1281
1287
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1282
- .pct_change(fill_method=cast(str, None))
1288
+ .pct_change()
1283
1289
  .quantile(1 - level, interpolation=interpolation)
1284
1290
  - self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1285
- .pct_change(fill_method=cast(str, None))
1291
+ .pct_change()
1286
1292
  .sum()
1287
1293
  / len(
1288
- self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(
1289
- fill_method=cast(str, None),
1290
- ),
1294
+ self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(),
1291
1295
  )
1292
1296
  )
1293
1297
  else:
1294
1298
  imp_vol = (
1295
1299
  -sqrt(time_factor)
1296
1300
  * self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1297
- .pct_change(fill_method=cast(str, None))
1301
+ .pct_change()
1298
1302
  .quantile(1 - level, interpolation=interpolation)
1299
1303
  / norm.ppf(level)
1300
1304
  )
@@ -1357,14 +1361,14 @@ class _CommonModel(BaseModel):
1357
1361
  cvar_df = self.tsdf.loc[cast(int, earlier) : cast(int, later)].copy(deep=True)
1358
1362
  result = [
1359
1363
  cvar_df.loc[:, x] # type: ignore[call-overload,index]
1360
- .pct_change(fill_method=cast(str, None))
1364
+ .pct_change()
1361
1365
  .sort_values()
1362
1366
  .iloc[
1363
1367
  : int(
1364
1368
  ceil(
1365
1369
  (1 - level)
1366
1370
  * cvar_df.loc[:, x] # type: ignore[index]
1367
- .pct_change(fill_method=cast(str, None))
1371
+ .pct_change()
1368
1372
  .count(),
1369
1373
  ),
1370
1374
  )
@@ -1424,7 +1428,7 @@ class _CommonModel(BaseModel):
1424
1428
  )
1425
1429
  how_many = (
1426
1430
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1427
- .pct_change(fill_method=cast(str, None))
1431
+ .pct_change()
1428
1432
  .count(numeric_only=True)
1429
1433
  )
1430
1434
  if periods_in_a_year_fixed:
@@ -1439,7 +1443,7 @@ class _CommonModel(BaseModel):
1439
1443
 
1440
1444
  dddf = (
1441
1445
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1442
- .pct_change(fill_method=cast(str, None))
1446
+ .pct_change()
1443
1447
  .sub(min_accepted_return / time_factor)
1444
1448
  )
1445
1449
 
@@ -1542,7 +1546,7 @@ class _CommonModel(BaseModel):
1542
1546
  )
1543
1547
  result: NDArray[float64] = skew(
1544
1548
  a=self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1545
- .pct_change(fill_method=cast(str, None))
1549
+ .pct_change()
1546
1550
  .to_numpy(),
1547
1551
  bias=True,
1548
1552
  nan_policy="omit",
@@ -1589,9 +1593,7 @@ class _CommonModel(BaseModel):
1589
1593
  to_dt=to_date,
1590
1594
  )
1591
1595
  result: NDArray[float64] = kurtosis(
1592
- self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(
1593
- fill_method=cast(str, None),
1594
- ),
1596
+ self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(),
1595
1597
  fisher=True,
1596
1598
  bias=True,
1597
1599
  nan_policy="omit",
@@ -1687,19 +1689,13 @@ class _CommonModel(BaseModel):
1687
1689
  )
1688
1690
  pos = (
1689
1691
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1690
- .pct_change(fill_method=cast(str, None))[1:][
1691
- self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(
1692
- fill_method=cast(str, None),
1693
- )[1:]
1692
+ .pct_change()[1:][
1693
+ self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change()[1:]
1694
1694
  > zero
1695
1695
  ]
1696
1696
  .count()
1697
1697
  )
1698
- tot = (
1699
- self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1700
- .pct_change(fill_method=cast(str, None))[1:]
1701
- .count()
1702
- )
1698
+ tot = self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change().count()
1703
1699
  share = pos / tot
1704
1700
  if self.tsdf.shape[1] == 1:
1705
1701
  return float(share.iloc[0])
@@ -1875,9 +1871,7 @@ class _CommonModel(BaseModel):
1875
1871
  from_dt=from_date,
1876
1872
  to_dt=to_date,
1877
1873
  )
1878
- retdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(
1879
- fill_method=cast(str, None),
1880
- )
1874
+ retdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change()
1881
1875
  pos = retdf[retdf > min_accepted_return].sub(min_accepted_return).sum()
1882
1876
  neg = retdf[retdf < min_accepted_return].sub(min_accepted_return).sum()
1883
1877
  ratio = pos / -neg
@@ -1965,7 +1959,7 @@ class _CommonModel(BaseModel):
1965
1959
  period = "-".join([str(year), str(month).zfill(2)])
1966
1960
  vrdf = self.tsdf.copy()
1967
1961
  vrdf.index = DatetimeIndex(vrdf.index)
1968
- resultdf = DataFrame(vrdf.pct_change(fill_method=None)) # type: ignore[arg-type]
1962
+ resultdf = DataFrame(vrdf.pct_change())
1969
1963
  result = resultdf.loc[period] + 1
1970
1964
  cal_period = result.cumprod(axis="index").iloc[-1] - 1
1971
1965
  if self.tsdf.shape[1] == 1:
@@ -2017,7 +2011,7 @@ class _CommonModel(BaseModel):
2017
2011
  )
2018
2012
  result = (
2019
2013
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
2020
- .pct_change(fill_method=cast(str, None))
2014
+ .pct_change()
2021
2015
  .quantile(1 - level, interpolation=interpolation)
2022
2016
  )
2023
2017
 
@@ -2065,7 +2059,7 @@ class _CommonModel(BaseModel):
2065
2059
  )
2066
2060
  result = (
2067
2061
  self.tsdf.loc[cast(int, earlier) : cast(int, later)]
2068
- .pct_change(fill_method=cast(str, None))
2062
+ .pct_change()
2069
2063
  .rolling(observations, min_periods=observations)
2070
2064
  .sum()
2071
2065
  .min()
@@ -2111,9 +2105,7 @@ class _CommonModel(BaseModel):
2111
2105
  from_dt=from_date,
2112
2106
  to_dt=to_date,
2113
2107
  )
2114
- zscframe = self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(
2115
- fill_method=cast(str, None),
2116
- )
2108
+ zscframe = self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change()
2117
2109
  result = (zscframe.iloc[-1] - zscframe.mean()) / zscframe.std()
2118
2110
 
2119
2111
  if self.tsdf.shape[1] == 1:
@@ -2182,7 +2174,7 @@ class _CommonModel(BaseModel):
2182
2174
  ret_label = cast(tuple[str], self.tsdf.iloc[:, column].name)[0]
2183
2175
  retseries = (
2184
2176
  self.tsdf.iloc[:, column]
2185
- .pct_change(fill_method=cast(str, None))
2177
+ .pct_change()
2186
2178
  .rolling(observations, min_periods=observations)
2187
2179
  .sum()
2188
2180
  )
@@ -2259,7 +2251,7 @@ class _CommonModel(BaseModel):
2259
2251
  else:
2260
2252
  time_factor = self.periods_in_a_year
2261
2253
  vol_label = cast(tuple[str, ValueType], self.tsdf.iloc[:, column].name)[0]
2262
- dframe = self.tsdf.iloc[:, column].pct_change(fill_method=cast(str, None))
2254
+ dframe = self.tsdf.iloc[:, column].pct_change()
2263
2255
  volseries = dframe.rolling(
2264
2256
  observations,
2265
2257
  min_periods=observations,
openseries/datefixer.py CHANGED
@@ -336,7 +336,7 @@ def generate_calendar_date_range(
336
336
  )
337
337
  calendar = holiday_calendar(
338
338
  startyear=start.year,
339
- endyear=tmp_range[-1].year,
339
+ endyear=date_fix(tmp_range[-1]).year,
340
340
  countries=countries,
341
341
  )
342
342
  return [
@@ -351,7 +351,7 @@ def generate_calendar_date_range(
351
351
  if end and not start:
352
352
  tmp_range = date_range(end=end, periods=trading_days * 365 // 252, freq="D")
353
353
  calendar = holiday_calendar(
354
- startyear=tmp_range[0].year,
354
+ startyear=date_fix(tmp_range[0]).year,
355
355
  endyear=end.year,
356
356
  countries=countries,
357
357
  )
@@ -371,7 +371,6 @@ def generate_calendar_date_range(
371
371
  raise ValueError(msg)
372
372
 
373
373
 
374
- # noinspection PyUnusedLocal
375
374
  def _do_resample_to_business_period_ends(
376
375
  data: DataFrame,
377
376
  freq: LiteralBizDayFreq,
@@ -421,4 +420,4 @@ def _do_resample_to_business_period_ends(
421
420
  ]
422
421
  + [copydata.index[-1]],
423
422
  )
424
- return dates.drop_duplicates()
423
+ return DatetimeIndex(dates.drop_duplicates())
openseries/frame.py CHANGED
@@ -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(fill_method=None)
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)
@@ -449,11 +455,19 @@ class OpenFrame(_CommonModel):
449
455
  countries=countries,
450
456
  )
451
457
  xerie.tsdf = xerie.tsdf.reindex(
452
- [deyt.date() for deyt in dates], method=method,
458
+ [deyt.date() for deyt in dates],
459
+ method=method,
453
460
  )
454
461
 
462
+ arrays = [
463
+ self.tsdf.columns.get_level_values(0),
464
+ self.tsdf.columns.get_level_values(1),
465
+ ]
466
+
455
467
  self._set_tsdf()
456
468
 
469
+ self.tsdf.columns = MultiIndex.from_arrays(arrays)
470
+
457
471
  return self
458
472
 
459
473
  def ewma_risk(
@@ -588,7 +602,7 @@ class OpenFrame(_CommonModel):
588
602
  Correlation matrix
589
603
 
590
604
  """
591
- corr_matrix = self.tsdf.pct_change(fill_method=None).corr(
605
+ corr_matrix = self.tsdf.pct_change().corr(
592
606
  method="pearson",
593
607
  min_periods=1,
594
608
  )
@@ -615,7 +629,6 @@ class OpenFrame(_CommonModel):
615
629
 
616
630
  """
617
631
  self.constituents += [new_series]
618
- # noinspection PyUnreachableCode
619
632
  self.tsdf = concat([self.tsdf, new_series.tsdf], axis="columns", sort=True)
620
633
  return self
621
634
 
@@ -814,10 +827,9 @@ class OpenFrame(_CommonModel):
814
827
  :,
815
828
  item,
816
829
  ]
817
- # noinspection PyTypeChecker
818
830
  relative = 1.0 + longdf - shortdf
819
831
  vol = float(
820
- relative.pct_change(fill_method=None).std() * sqrt(time_factor),
832
+ relative.pct_change().std() * sqrt(time_factor),
821
833
  )
822
834
  terrors.append(vol)
823
835
 
@@ -905,13 +917,12 @@ class OpenFrame(_CommonModel):
905
917
  :,
906
918
  item,
907
919
  ]
908
- # noinspection PyTypeChecker
909
920
  relative = 1.0 + longdf - shortdf
910
921
  ret = float(
911
- relative.pct_change(fill_method=None).mean() * time_factor,
922
+ relative.pct_change().mean() * time_factor,
912
923
  )
913
924
  vol = float(
914
- relative.pct_change(fill_method=None).std() * sqrt(time_factor),
925
+ relative.pct_change().std() * sqrt(time_factor),
915
926
  )
916
927
  ratios.append(ret / vol)
917
928
 
@@ -1010,18 +1021,16 @@ class OpenFrame(_CommonModel):
1010
1021
  msg = "ratio must be one of 'up', 'down' or 'both'."
1011
1022
  if ratio == "up":
1012
1023
  uparray = (
1013
- longdf.pct_change(fill_method=None)[
1014
- shortdf.pct_change(fill_method=None).to_numpy()
1015
- > loss_limit
1024
+ longdf.pct_change()[
1025
+ shortdf.pct_change().to_numpy() > loss_limit
1016
1026
  ]
1017
1027
  .add(1)
1018
1028
  .to_numpy()
1019
1029
  )
1020
1030
  up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
1021
1031
  upidxarray = (
1022
- shortdf.pct_change(fill_method=None)[
1023
- shortdf.pct_change(fill_method=None).to_numpy()
1024
- > loss_limit
1032
+ shortdf.pct_change()[
1033
+ shortdf.pct_change().to_numpy() > loss_limit
1025
1034
  ]
1026
1035
  .add(1)
1027
1036
  .to_numpy()
@@ -1032,9 +1041,8 @@ class OpenFrame(_CommonModel):
1032
1041
  ratios.append(up_rtrn / up_idx_return)
1033
1042
  elif ratio == "down":
1034
1043
  downarray = (
1035
- longdf.pct_change(fill_method=None)[
1036
- shortdf.pct_change(fill_method=None).to_numpy()
1037
- < loss_limit
1044
+ longdf.pct_change()[
1045
+ shortdf.pct_change().to_numpy() < loss_limit
1038
1046
  ]
1039
1047
  .add(1)
1040
1048
  .to_numpy()
@@ -1043,9 +1051,8 @@ class OpenFrame(_CommonModel):
1043
1051
  downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
1044
1052
  )
1045
1053
  downidxarray = (
1046
- shortdf.pct_change(fill_method=None)[
1047
- shortdf.pct_change(fill_method=None).to_numpy()
1048
- < loss_limit
1054
+ shortdf.pct_change()[
1055
+ shortdf.pct_change().to_numpy() < loss_limit
1049
1056
  ]
1050
1057
  .add(1)
1051
1058
  .to_numpy()
@@ -1057,18 +1064,16 @@ class OpenFrame(_CommonModel):
1057
1064
  ratios.append(down_return / down_idx_return)
1058
1065
  elif ratio == "both":
1059
1066
  uparray = (
1060
- longdf.pct_change(fill_method=None)[
1061
- shortdf.pct_change(fill_method=None).to_numpy()
1062
- > loss_limit
1067
+ longdf.pct_change()[
1068
+ shortdf.pct_change().to_numpy() > loss_limit
1063
1069
  ]
1064
1070
  .add(1)
1065
1071
  .to_numpy()
1066
1072
  )
1067
1073
  up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
1068
1074
  upidxarray = (
1069
- shortdf.pct_change(fill_method=None)[
1070
- shortdf.pct_change(fill_method=None).to_numpy()
1071
- > loss_limit
1075
+ shortdf.pct_change()[
1076
+ shortdf.pct_change().to_numpy() > loss_limit
1072
1077
  ]
1073
1078
  .add(1)
1074
1079
  .to_numpy()
@@ -1077,9 +1082,8 @@ class OpenFrame(_CommonModel):
1077
1082
  upidxarray.prod() ** (1 / (len(upidxarray) / time_factor)) - 1
1078
1083
  )
1079
1084
  downarray = (
1080
- longdf.pct_change(fill_method=None)[
1081
- shortdf.pct_change(fill_method=None).to_numpy()
1082
- < loss_limit
1085
+ longdf.pct_change()[
1086
+ shortdf.pct_change().to_numpy() < loss_limit
1083
1087
  ]
1084
1088
  .add(1)
1085
1089
  .to_numpy()
@@ -1088,9 +1092,8 @@ class OpenFrame(_CommonModel):
1088
1092
  downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
1089
1093
  )
1090
1094
  downidxarray = (
1091
- shortdf.pct_change(fill_method=None)[
1092
- shortdf.pct_change(fill_method=None).to_numpy()
1093
- < loss_limit
1095
+ shortdf.pct_change()[
1096
+ shortdf.pct_change().to_numpy() < loss_limit
1094
1097
  ]
1095
1098
  .add(1)
1096
1099
  .to_numpy()
@@ -1294,30 +1297,8 @@ class OpenFrame(_CommonModel):
1294
1297
 
1295
1298
  """
1296
1299
  full_year = 1.0
1297
- if all(
1298
- x == ValueType.RTRN
1299
- for x in self.tsdf.columns.get_level_values(1).to_numpy()
1300
- ):
1301
- msg = "asset should be a tuple[str, ValueType] or an integer."
1302
- if isinstance(asset, tuple):
1303
- asset_log = self.tsdf.loc[:, asset]
1304
- asset_cagr = asset_log.mean()
1305
- elif isinstance(asset, int):
1306
- asset_log = self.tsdf.iloc[:, asset]
1307
- asset_cagr = asset_log.mean()
1308
- else:
1309
- raise TypeError(msg)
1310
-
1311
- msg = "market should be a tuple[str, ValueType] or an integer."
1312
- if isinstance(market, tuple):
1313
- market_log = self.tsdf.loc[:, market]
1314
- market_cagr = market_log.mean()
1315
- elif isinstance(market, int):
1316
- market_log = self.tsdf.iloc[:, market]
1317
- market_cagr = market_log.mean()
1318
- else:
1319
- raise TypeError(msg)
1320
- else:
1300
+ vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
1301
+ if not any(vtypes):
1321
1302
  msg = "asset should be a tuple[str, ValueType] or an integer."
1322
1303
  if isinstance(asset, tuple):
1323
1304
  asset_log = log(
@@ -1385,6 +1366,29 @@ class OpenFrame(_CommonModel):
1385
1366
  )
1386
1367
  else:
1387
1368
  raise TypeError(msg)
1369
+ elif all(vtypes):
1370
+ msg = "asset should be a tuple[str, ValueType] or an integer."
1371
+ if isinstance(asset, tuple):
1372
+ asset_log = self.tsdf.loc[:, asset]
1373
+ asset_cagr = asset_log.mean()
1374
+ elif isinstance(asset, int):
1375
+ asset_log = self.tsdf.iloc[:, asset]
1376
+ asset_cagr = asset_log.mean()
1377
+ else:
1378
+ raise TypeError(msg)
1379
+
1380
+ msg = "market should be a tuple[str, ValueType] or an integer."
1381
+ if isinstance(market, tuple):
1382
+ market_log = self.tsdf.loc[:, market]
1383
+ market_cagr = market_log.mean()
1384
+ elif isinstance(market, int):
1385
+ market_log = self.tsdf.iloc[:, market]
1386
+ market_cagr = market_log.mean()
1387
+ else:
1388
+ raise TypeError(msg)
1389
+ else:
1390
+ msg = "Mix of series types will give inconsistent results"
1391
+ raise ValueError(msg)
1388
1392
 
1389
1393
  covariance = cov(asset_log, market_log, ddof=dlta_degr_freedms)
1390
1394
  beta = covariance[0, 1] / covariance[1, 1]
@@ -1417,27 +1421,30 @@ class OpenFrame(_CommonModel):
1417
1421
  "to run the make_portfolio method."
1418
1422
  )
1419
1423
  raise ValueError(msg)
1420
- dframe = self.tsdf.copy()
1421
- if not any(
1422
- x == ValueType.RTRN
1423
- for x in self.tsdf.columns.get_level_values(1).to_numpy()
1424
- ):
1425
- dframe = dframe.pct_change(fill_method=None)
1426
- dframe.iloc[0] = 0
1424
+
1425
+ vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
1426
+ if not any(vtypes):
1427
+ returns = self.tsdf.ffill().pct_change()
1428
+ returns.iloc[0] = 0
1429
+ elif all(vtypes):
1430
+ returns = self.tsdf.copy()
1431
+ else:
1432
+ msg = "Mix of series types will give inconsistent results"
1433
+ raise ValueError(msg)
1427
1434
 
1428
1435
  msg = "Weight strategy not implemented"
1429
1436
  if weight_strat:
1430
1437
  if weight_strat == "eq_weights":
1431
1438
  self.weights = [1.0 / self.item_count] * self.item_count
1432
1439
  elif weight_strat == "inv_vol":
1433
- vol = divide(1.0, std(dframe, axis=0, ddof=1))
1440
+ vol = divide(1.0, std(returns, axis=0, ddof=1))
1434
1441
  vol[isinf(vol)] = nan
1435
1442
  self.weights = list(divide(vol, vol.sum()))
1436
1443
  else:
1437
1444
  raise NotImplementedError(msg)
1438
1445
 
1439
1446
  return DataFrame(
1440
- data=(dframe @ self.weights).add(1.0).cumprod(),
1447
+ data=(returns @ self.weights).add(1.0).cumprod(),
1441
1448
  index=self.tsdf.index,
1442
1449
  columns=[[name], [ValueType.PRICE]],
1443
1450
  dtype="float64",
@@ -1492,13 +1499,11 @@ class OpenFrame(_CommonModel):
1492
1499
  )
1493
1500
 
1494
1501
  retseries = (
1495
- relative.pct_change(fill_method=None)
1496
- .rolling(observations, min_periods=observations)
1497
- .sum()
1502
+ relative.pct_change().rolling(observations, min_periods=observations).sum()
1498
1503
  )
1499
1504
  retdf = retseries.dropna().to_frame()
1500
1505
 
1501
- voldf = relative.pct_change(fill_method=None).rolling(
1506
+ voldf = relative.pct_change().rolling(
1502
1507
  observations,
1503
1508
  min_periods=observations,
1504
1509
  ).std() * sqrt(time_factor)
@@ -1542,7 +1547,7 @@ class OpenFrame(_CommonModel):
1542
1547
  asset_label = cast(tuple[str, str], self.tsdf.iloc[:, asset_column].name)[0]
1543
1548
  beta_label = f"{asset_label} / {market_label}"
1544
1549
 
1545
- rolling = self.tsdf.pct_change(fill_method=None).rolling(
1550
+ rolling = self.tsdf.pct_change().rolling(
1546
1551
  observations,
1547
1552
  min_periods=observations,
1548
1553
  )
@@ -1599,12 +1604,10 @@ class OpenFrame(_CommonModel):
1599
1604
  )
1600
1605
  first_series = (
1601
1606
  self.tsdf.iloc[:, first_column]
1602
- .pct_change(fill_method=None)[1:]
1607
+ .pct_change()[1:]
1603
1608
  .rolling(observations, min_periods=observations)
1604
1609
  )
1605
- second_series = self.tsdf.iloc[:, second_column].pct_change(
1606
- fill_method=None,
1607
- )[1:]
1610
+ second_series = self.tsdf.iloc[:, second_column].pct_change()[1:]
1608
1611
  corrdf = first_series.corr(other=second_series).dropna().to_frame()
1609
1612
  corrdf.columns = MultiIndex.from_arrays(
1610
1613
  [
@@ -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
 
@@ -105,15 +107,14 @@ def simulate_portfolios(
105
107
  weights = weights / npsum(weights)
106
108
  all_weights[x, :] = weights
107
109
 
108
- vol_arr[x] = sqrt(weights.T @
109
- (log_ret.cov() * simframe.periods_in_a_year @ weights),
110
+ vol_arr[x] = sqrt(
111
+ weights.T @ (log_ret.cov() * simframe.periods_in_a_year @ weights),
110
112
  )
111
113
 
112
114
  ret_arr[x] = npsum(log_ret.mean() * weights * simframe.periods_in_a_year)
113
115
 
114
116
  sharpe_arr[x] = ret_arr[x] / vol_arr[x]
115
117
 
116
- # noinspection PyUnreachableCode
117
118
  simdf = concat(
118
119
  [
119
120
  DataFrame({"stdev": vol_arr, "ret": ret_arr, "sharpe": sharpe_arr}),
@@ -166,13 +167,15 @@ def efficient_frontier( # noqa: C901
166
167
 
167
168
  copi = eframe.from_deepcopy()
168
169
 
169
- if any(
170
- x == ValueType.PRICE for x in copi.tsdf.columns.get_level_values(1).to_numpy()
171
- ):
170
+ vtypes = [x == ValueType.RTRN for x in copi.tsdf.columns.get_level_values(1)]
171
+ if not any(vtypes):
172
172
  copi.value_to_ret()
173
173
  log_ret = copi.tsdf.copy()[1:]
174
- else:
174
+ elif all(vtypes):
175
175
  log_ret = copi.tsdf.copy()
176
+ else:
177
+ msg = "Mix of series types will give inconsistent results"
178
+ raise ValueError(msg)
176
179
 
177
180
  log_ret.columns = log_ret.columns.droplevel(level=1)
178
181
 
@@ -284,7 +287,6 @@ def efficient_frontier( # noqa: C901
284
287
  frontier_x.append(result["fun"])
285
288
  frontier_weights.append(result["x"])
286
289
 
287
- # noinspection PyUnreachableCode
288
290
  line_df = concat(
289
291
  [
290
292
  DataFrame(data=frontier_weights, columns=eframe.columns_lvl_zero),
openseries/series.py CHANGED
@@ -205,7 +205,7 @@ class OpenTimeSeries(_CommonModel):
205
205
  @classmethod
206
206
  def from_df(
207
207
  cls: type[OpenTimeSeries],
208
- dframe: DataFrame | Series[float],
208
+ dframe: Series[float] | DataFrame,
209
209
  column_nmbr: int = 0,
210
210
  valuetype: ValueType = ValueType.PRICE,
211
211
  baseccy: CurrencyStringType = "SEK",
@@ -234,10 +234,8 @@ class OpenTimeSeries(_CommonModel):
234
234
 
235
235
  """
236
236
  msg = "Argument dframe must be pandas Series or DataFrame."
237
- if isinstance(dframe, Series): # type: ignore[unreachable,unused-ignore]
238
- if isinstance( # type: ignore[unreachable,unused-ignore]
239
- dframe.name, tuple,
240
- ):
237
+ if isinstance(dframe, Series):
238
+ if isinstance(dframe.name, tuple):
241
239
  label, _ = dframe.name
242
240
  else:
243
241
  label = dframe.name
@@ -348,6 +346,7 @@ class OpenTimeSeries(_CommonModel):
348
346
  - cast(DatetimeIndex, d_range)[:-1]
349
347
  ],
350
348
  )
349
+ # noinspection PyTypeChecker
351
350
  arr = list(cumprod(insert(1 + deltas * rate / 365, 0, 1.0)))
352
351
  dates = [d.strftime("%Y-%m-%d") for d in cast(DatetimeIndex, d_range)]
353
352
 
@@ -436,15 +435,12 @@ class OpenTimeSeries(_CommonModel):
436
435
  The returns of the values in the series
437
436
 
438
437
  """
439
- self.tsdf = self.tsdf.pct_change(fill_method=None) # type: ignore[arg-type]
440
- self.tsdf.iloc[0] = 0
438
+ returns = self.tsdf.ffill().pct_change()
439
+ returns.iloc[0] = 0
441
440
  self.valuetype = ValueType.RTRN
442
- self.tsdf.columns = MultiIndex.from_arrays(
443
- [
444
- [self.label],
445
- [self.valuetype],
446
- ],
447
- )
441
+ arrays = [[self.label], [self.valuetype]]
442
+ returns.columns = MultiIndex.from_arrays(arrays=arrays)
443
+ self.tsdf = returns.copy()
448
444
  return self
449
445
 
450
446
  def value_to_diff(self: Self, periods: int = 1) -> Self:
@@ -482,14 +478,12 @@ class OpenTimeSeries(_CommonModel):
482
478
  An OpenTimeSeries object
483
479
 
484
480
  """
485
- if not any(
486
- x == ValueType.RTRN
487
- for x in cast(MultiIndex, self.tsdf.columns).get_level_values(1).to_numpy()
488
- ):
481
+ if self.valuetype == ValueType.PRICE:
489
482
  self.value_to_ret()
490
483
 
491
484
  self.tsdf = self.tsdf.add(1.0)
492
485
  self.tsdf = self.tsdf.cumprod(axis=0) / self.tsdf.iloc[0]
486
+
493
487
  self.valuetype = ValueType.PRICE
494
488
  self.tsdf.columns = MultiIndex.from_arrays(
495
489
  [
@@ -522,6 +516,7 @@ class OpenTimeSeries(_CommonModel):
522
516
  arr = array(self.values) / divider
523
517
 
524
518
  deltas = array([i.days for i in self.tsdf.index[1:] - self.tsdf.index[:-1]])
519
+ # noinspection PyTypeChecker
525
520
  arr = cumprod(insert(1.0 + deltas * arr[:-1] / days_in_year, 0, 1.0))
526
521
 
527
522
  self.dates = [d.strftime("%Y-%m-%d") for d in self.tsdf.index]
@@ -686,17 +681,13 @@ class OpenTimeSeries(_CommonModel):
686
681
  An OpenTimeSeries object
687
682
 
688
683
  """
689
- values: list[float]
690
- if any(
691
- x == ValueType.RTRN
692
- for x in cast(MultiIndex, self.tsdf.columns).get_level_values(1).to_numpy()
693
- ):
684
+ if self.valuetype == ValueType.RTRN:
694
685
  ra_df = self.tsdf.copy()
695
686
  values = [1.0]
696
687
  returns_input = True
697
688
  else:
698
689
  values = [cast(float, self.tsdf.iloc[0, 0])]
699
- ra_df = self.tsdf.pct_change(fill_method=None) # type: ignore[arg-type]
690
+ ra_df = self.tsdf.pct_change()
700
691
  returns_input = False
701
692
  ra_df = ra_df.dropna()
702
693
 
@@ -820,6 +811,7 @@ def timeseries_chain(
820
811
 
821
812
  dates.extend([x.strftime("%Y-%m-%d") for x in new.tsdf.index])
822
813
 
814
+ # noinspection PyUnresolvedReferences
823
815
  if back.__class__.__subclasscheck__(
824
816
  OpenTimeSeries,
825
817
  ):
openseries/simulation.py CHANGED
@@ -122,10 +122,7 @@ class ReturnSimulation(BaseModel):
122
122
  """
123
123
  return cast(
124
124
  float,
125
- (
126
- self.results.pct_change(fill_method=cast(str, None)).mean()
127
- * self.trading_days_in_year
128
- ).iloc[0],
125
+ (self.results.pct_change().mean() * self.trading_days_in_year).iloc[0],
129
126
  )
130
127
 
131
128
  @property
@@ -140,10 +137,9 @@ class ReturnSimulation(BaseModel):
140
137
  """
141
138
  return cast(
142
139
  float,
143
- (
144
- self.results.pct_change(fill_method=cast(str, None)).std()
145
- * sqrt(self.trading_days_in_year)
146
- ).iloc[0],
140
+ (self.results.pct_change().std() * sqrt(self.trading_days_in_year)).iloc[
141
+ 0
142
+ ],
147
143
  )
148
144
 
149
145
  @classmethod
openseries/types.py CHANGED
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import datetime as dt
6
6
  from enum import Enum
7
+ from pprint import pformat
7
8
  from typing import Annotated, ClassVar, Literal, Union
8
9
 
9
10
  from numpy import datetime64
@@ -225,8 +226,8 @@ LiteralFrameProps = Literal[
225
226
  ]
226
227
 
227
228
 
228
- class OpenTimeSeriesPropertiesList(list[str]):
229
- """Allowed property arguments for the OpenTimeSeries class."""
229
+ class PropertiesList(list[str]):
230
+ """Base class for allowed property arguments definition."""
230
231
 
231
232
  allowed_strings: ClassVar[set[str]] = {
232
233
  "value_ret",
@@ -246,9 +247,38 @@ class OpenTimeSeriesPropertiesList(list[str]):
246
247
  "vol_from_var",
247
248
  "worst",
248
249
  "worst_month",
249
- "max_drawdown_cal_year",
250
250
  "max_drawdown",
251
251
  "max_drawdown_date",
252
+ "max_drawdown_cal_year",
253
+ }
254
+
255
+ def _validate(self: Self) -> None:
256
+ """Validate the string input of the all_properties method."""
257
+ seen = set()
258
+ invalids = set()
259
+ duplicates = set()
260
+ msg = ""
261
+ for item in self:
262
+ if item not in self.allowed_strings:
263
+ invalids.add(item)
264
+ if item in seen:
265
+ duplicates.add(item)
266
+ seen.add(item)
267
+ if len(invalids) != 0:
268
+ msg += (
269
+ f"Invalid string(s): {list(invalids)}.\nAllowed strings are:"
270
+ f"\n{pformat(self.allowed_strings)}\n"
271
+ )
272
+ if len(duplicates) != 0:
273
+ msg += f"Duplicate string(s): {list(duplicates)}."
274
+ if len(msg) != 0:
275
+ raise ValueError(msg)
276
+
277
+
278
+ class OpenTimeSeriesPropertiesList(PropertiesList):
279
+ """Allowed property arguments for the OpenTimeSeries class."""
280
+
281
+ allowed_strings: ClassVar[set[str]] = PropertiesList.allowed_strings | {
252
282
  "first_idx",
253
283
  "last_idx",
254
284
  "length",
@@ -265,44 +295,11 @@ class OpenTimeSeriesPropertiesList(list[str]):
265
295
  super().__init__(args)
266
296
  self._validate()
267
297
 
268
- def _validate(self: Self) -> None:
269
- seen = set()
270
- for item in self:
271
- if item not in self.allowed_strings:
272
- msg = (
273
- f"Invalid string: {item}. Allowed strings: {self.allowed_strings}"
274
- )
275
- raise ValueError(msg)
276
- if item in seen:
277
- msg = f"Duplicate string: {item}"
278
- raise ValueError(msg)
279
- seen.add(item)
280
-
281
298
 
282
- class OpenFramePropertiesList(list[str]):
299
+ class OpenFramePropertiesList(PropertiesList):
283
300
  """Allowed property arguments for the OpenFrame class."""
284
301
 
285
- allowed_strings: ClassVar[set[str]] = {
286
- "value_ret",
287
- "geo_ret",
288
- "arithmetic_ret",
289
- "vol",
290
- "downside_deviation",
291
- "ret_vol_ratio",
292
- "sortino_ratio",
293
- "omega_ratio",
294
- "z_score",
295
- "skew",
296
- "kurtosis",
297
- "positive_share",
298
- "var_down",
299
- "cvar_down",
300
- "vol_from_var",
301
- "worst",
302
- "worst_month",
303
- "max_drawdown",
304
- "max_drawdown_date",
305
- "max_drawdown_cal_year",
302
+ allowed_strings: ClassVar[set[str]] = PropertiesList.allowed_strings | {
306
303
  "first_indices",
307
304
  "last_indices",
308
305
  "lengths_of_items",
@@ -314,19 +311,6 @@ class OpenFramePropertiesList(list[str]):
314
311
  super().__init__(args)
315
312
  self._validate()
316
313
 
317
- def _validate(self: Self) -> None:
318
- seen = set()
319
- for item in self:
320
- if item not in self.allowed_strings:
321
- msg = (
322
- f"Invalid string: {item}. Allowed strings: {self.allowed_strings}"
323
- )
324
- raise ValueError(msg)
325
- if item in seen:
326
- msg = f"Duplicate string: {item}"
327
- raise ValueError(msg)
328
- seen.add(item)
329
-
330
314
 
331
315
  class ValueType(str, Enum):
332
316
  """Enum types of OpenTimeSeries to identify the output."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openseries
3
- Version: 1.7.2
3
+ Version: 1.7.4
4
4
  Summary: Tools for analyzing financial timeseries.
5
5
  Home-page: https://github.com/CaptorAB/openseries
6
6
  License: BSD-3-Clause
@@ -28,7 +28,7 @@ Requires-Dist: pyarrow (>=14.0.2,<18.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
- Requires-Dist: scipy (>=1.11.4,<1.14.1)
31
+ Requires-Dist: scipy (>=1.11.4,<2.0.0)
32
32
  Requires-Dist: statsmodels (>=0.14.0,<1.0.0)
33
33
  Project-URL: Repository, https://github.com/CaptorAB/openseries
34
34
  Description-Content-Type: text/markdown
@@ -0,0 +1,16 @@
1
+ openseries/__init__.py,sha256=gD2dMKRTJ9HMXLca_5sR67xGiU5sWExwaNUi-9N_RGQ,1032
2
+ openseries/_common_model.py,sha256=kCP2UWRwnlh4Byj6qUKEddW5HhSh6z5tD6xinJNNtQE,72863
3
+ openseries/_risk.py,sha256=PReIfkzhInvIgJkzI4k1wYvhmLZ4cCurYKuQAvlHLlE,2082
4
+ openseries/datefixer.py,sha256=3E8Ddf3Ps9k5qUWqL13ONelyMECZsvtyrsFJg7r3bxE,12298
5
+ openseries/frame.py,sha256=3f-vXWhQgmApJ4FSLYXydHSxutI1uT0dLJSF9zASlfs,54784
6
+ openseries/load_plotly.py,sha256=Uuk_-iIY4_C6Z5U3rAneOh8ZlGYWbkuis9s4Amxzko4,1921
7
+ openseries/plotly_captor_logo.json,sha256=F5nhMzEyxKywtjvQqMTKgKRCJQYMDIiBgDSxdte8Clo,178
8
+ openseries/plotly_layouts.json,sha256=ahx8-dL4_RPzvHtBOX0SiL0AH7xQJzNRSDhGrSmU-Og,1429
9
+ openseries/portfoliotools.py,sha256=pyCXnNLTnd9MGtmRIJZ-bz4fccOuUsXSX-3AbSpwa2Y,19094
10
+ openseries/series.py,sha256=NSL3wFqoXgGLlGX-9t6QJo3QPOXJWV7yAn2_oIY-SaU,27646
11
+ openseries/simulation.py,sha256=jghKf0d2deXK-rqn8BeJ_s3JQVLL9FuE_IW29Kf1_-k,13816
12
+ openseries/types.py,sha256=IbzW9iVuA3MEx3WtjyB6QYeSd3TkGjTOkFnnpSADKlY,7491
13
+ openseries-1.7.4.dist-info/LICENSE.md,sha256=IQ8_IMXgHxyv4M48G14fJsjcrkiSASdalASTXWCOsj4,1515
14
+ openseries-1.7.4.dist-info/METADATA,sha256=Is5FxVodV6ix3yUHuljPgP8fP3EAtU3H4u5h9Q-RCRE,43973
15
+ openseries-1.7.4.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
16
+ openseries-1.7.4.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- openseries/__init__.py,sha256=gD2dMKRTJ9HMXLca_5sR67xGiU5sWExwaNUi-9N_RGQ,1032
2
- openseries/_common_model.py,sha256=QuwlYSGCaTr6q0pPiHg6ni63ZvA8I2HfbgqSj4HmBHY,73657
3
- openseries/_risk.py,sha256=PReIfkzhInvIgJkzI4k1wYvhmLZ4cCurYKuQAvlHLlE,2082
4
- openseries/datefixer.py,sha256=IJiR43fVAf7hLJk0-5-xzQXNv6vAYxoJgRHH_EM1a9c,12292
5
- openseries/frame.py,sha256=WNih33lji-oHkHQMLqMQ3CWz-jmfS4eLQrcXE0wzbys,54885
6
- openseries/load_plotly.py,sha256=Uuk_-iIY4_C6Z5U3rAneOh8ZlGYWbkuis9s4Amxzko4,1921
7
- openseries/plotly_captor_logo.json,sha256=F5nhMzEyxKywtjvQqMTKgKRCJQYMDIiBgDSxdte8Clo,178
8
- openseries/plotly_layouts.json,sha256=ahx8-dL4_RPzvHtBOX0SiL0AH7xQJzNRSDhGrSmU-Og,1429
9
- openseries/portfoliotools.py,sha256=WQSf2OdrSojx-uOhLWQzz6IMnCOZjWjh-8yPJmqTZcA,18933
10
- openseries/series.py,sha256=PaXIVp7dLM4VsxPOe4B8tqM1MMDtJtPSDFvhv1Yblhc,27971
11
- openseries/simulation.py,sha256=P5nkX7o3O_3pOL22MAZTqL_i4TB2IUmPGCodegsAM04,13932
12
- openseries/types.py,sha256=C_t7WLyc7BgKun728CpLXnJS1GisGdNnxYAMBNAcVXU,7830
13
- openseries-1.7.2.dist-info/LICENSE.md,sha256=IQ8_IMXgHxyv4M48G14fJsjcrkiSASdalASTXWCOsj4,1515
14
- openseries-1.7.2.dist-info/METADATA,sha256=58Cg-cbff8KR3P_vvGUMEFvJg3SEy271FXNhEbIQudI,43974
15
- openseries-1.7.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
16
- openseries-1.7.2.dist-info/RECORD,,