openseries 1.9.3__py3-none-any.whl → 1.9.5__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.
openseries/frame.py CHANGED
@@ -7,7 +7,6 @@ https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
7
7
  SPDX-License-Identifier: BSD-3-Clause
8
8
  """
9
9
 
10
- # mypy: disable-error-code="assignment,no-any-return"
11
10
  from __future__ import annotations
12
11
 
13
12
  from copy import deepcopy
@@ -18,6 +17,8 @@ from typing import TYPE_CHECKING, Any, cast
18
17
  if TYPE_CHECKING: # pragma: no cover
19
18
  import datetime as dt
20
19
 
20
+ from numpy import dtype, int64, ndarray
21
+
21
22
  from numpy import (
22
23
  array,
23
24
  cov,
@@ -38,7 +39,7 @@ from pandas import (
38
39
  merge,
39
40
  )
40
41
  from pydantic import field_validator
41
- from sklearn.linear_model import LinearRegression
42
+ from sklearn.linear_model import LinearRegression # type: ignore[import-untyped]
42
43
 
43
44
  from ._common_model import _CommonModel
44
45
  from .datefixer import _do_resample_to_business_period_ends
@@ -57,6 +58,7 @@ from .owntypes import (
57
58
  NoWeightsError,
58
59
  OpenFramePropertiesList,
59
60
  RatioInputError,
61
+ ResampleDataLossError,
60
62
  Self,
61
63
  ValueType,
62
64
  )
@@ -68,7 +70,7 @@ __all__ = ["OpenFrame"]
68
70
 
69
71
 
70
72
  # noinspection PyUnresolvedReferences,PyTypeChecker
71
- class OpenFrame(_CommonModel): # type: ignore[misc]
73
+ class OpenFrame(_CommonModel):
72
74
  """OpenFrame objects hold OpenTimeSeries in the list constituents.
73
75
 
74
76
  The intended use is to allow comparisons across these timeseries.
@@ -93,7 +95,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
93
95
 
94
96
  # noinspection PyMethodParameters
95
97
  @field_validator("constituents") # type: ignore[misc]
96
- def _check_labels_unique( # type: ignore[misc]
98
+ def _check_labels_unique(
97
99
  cls: OpenFrame, # noqa: N805
98
100
  tseries: list[OpenTimeSeries],
99
101
  ) -> list[OpenTimeSeries]:
@@ -128,7 +130,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
128
130
  """
129
131
  copied_constituents = [ts.from_deepcopy() for ts in constituents]
130
132
 
131
- super().__init__( # type: ignore[call-arg]
133
+ super().__init__(
132
134
  constituents=copied_constituents,
133
135
  weights=weights,
134
136
  )
@@ -340,7 +342,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
340
342
  The returns of the values in the series
341
343
 
342
344
  """
343
- returns = self.tsdf.pct_change()
345
+ returns = self.tsdf.ffill().pct_change()
344
346
  returns.iloc[0] = 0
345
347
  new_labels: list[ValueType] = [ValueType.RTRN] * self.item_count
346
348
  arrays: list[Index[Any], list[ValueType]] = [ # type: ignore[type-arg]
@@ -387,7 +389,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
387
389
  """
388
390
  vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
389
391
  if not any(vtypes):
390
- returns = self.tsdf.pct_change()
392
+ returns = self.tsdf.ffill().pct_change()
391
393
  returns.iloc[0] = 0
392
394
  elif all(vtypes):
393
395
  returns = self.tsdf.copy()
@@ -424,12 +426,27 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
424
426
  An OpenFrame object
425
427
 
426
428
  """
429
+ vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
430
+ if not any(vtypes):
431
+ value_type = ValueType.PRICE
432
+ elif all(vtypes):
433
+ value_type = ValueType.RTRN
434
+ else:
435
+ msg = "Mix of series types will give inconsistent results"
436
+ raise MixedValuetypesError(msg)
437
+
427
438
  self.tsdf.index = DatetimeIndex(self.tsdf.index)
428
- self.tsdf = self.tsdf.resample(freq).last()
439
+ if value_type == ValueType.PRICE:
440
+ self.tsdf = self.tsdf.resample(freq).last()
441
+ else:
442
+ self.tsdf = self.tsdf.resample(freq).sum()
429
443
  self.tsdf.index = Index(d.date() for d in DatetimeIndex(self.tsdf.index))
430
444
  for xerie in self.constituents:
431
445
  xerie.tsdf.index = DatetimeIndex(xerie.tsdf.index)
432
- xerie.tsdf = xerie.tsdf.resample(freq).last()
446
+ if value_type == ValueType.PRICE:
447
+ xerie.tsdf = xerie.tsdf.resample(freq).last()
448
+ else:
449
+ xerie.tsdf = xerie.tsdf.resample(freq).sum()
433
450
  xerie.tsdf.index = Index(
434
451
  dejt.date() for dejt in DatetimeIndex(xerie.tsdf.index)
435
452
  )
@@ -458,6 +475,15 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
458
475
  An OpenFrame object
459
476
 
460
477
  """
478
+ vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
479
+ if any(vtypes):
480
+ msg = (
481
+ "Do not run resample_to_business_period_ends on return series. "
482
+ "The operation will pick the last data point in the sparser series. "
483
+ "It will not sum returns and therefore data will be lost."
484
+ )
485
+ raise ResampleDataLossError(msg)
486
+
461
487
  for xerie in self.constituents:
462
488
  dates = _do_resample_to_business_period_ends(
463
489
  data=xerie.tsdf,
@@ -530,7 +556,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
530
556
  Series volatilities and correlation
531
557
 
532
558
  """
533
- earlier, later = self.calc_range(months_from_last, from_date, to_date)
559
+ earlier, later = self.calc_range(
560
+ months_offset=months_from_last, from_dt=from_date, to_dt=to_date
561
+ )
534
562
  if periods_in_a_year_fixed is None:
535
563
  fraction = (later - earlier).days / 365.25
536
564
  how_many = (
@@ -621,9 +649,13 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
621
649
  Correlation matrix
622
650
 
623
651
  """
624
- corr_matrix = self.tsdf.pct_change().corr(
625
- method="pearson",
626
- min_periods=1,
652
+ corr_matrix = (
653
+ self.tsdf.ffill()
654
+ .pct_change()
655
+ .corr(
656
+ method="pearson",
657
+ min_periods=1,
658
+ )
627
659
  )
628
660
  corr_matrix.columns = corr_matrix.columns.droplevel(level=1)
629
661
  corr_matrix.index = corr_matrix.index.droplevel(level=1)
@@ -805,7 +837,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
805
837
  Tracking Errors
806
838
 
807
839
  """
808
- earlier, later = self.calc_range(months_from_last, from_date, to_date)
840
+ earlier, later = self.calc_range(
841
+ months_offset=months_from_last, from_dt=from_date, to_dt=to_date
842
+ )
809
843
  fraction = (later - earlier).days / 365.25
810
844
 
811
845
  msg = "base_column should be a tuple[str, ValueType] or an integer."
@@ -820,14 +854,15 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
820
854
  self.tsdf.loc[:, base_column].name,
821
855
  )[0]
822
856
  elif isinstance(base_column, int):
823
- shortdf = self.tsdf.loc[cast("int", earlier) : cast("int", later)].iloc[
824
- :,
825
- base_column,
826
- ]
827
- short_item = self.tsdf.iloc[
828
- :,
829
- base_column,
830
- ].name
857
+ shortdf = cast(
858
+ "DataFrame",
859
+ self.tsdf.loc[cast("int", earlier) : cast("int", later)].iloc[
860
+ :, base_column
861
+ ],
862
+ )
863
+ short_item = cast(
864
+ "tuple[str, ValueType]", self.tsdf.iloc[:, base_column].name
865
+ )
831
866
  short_label = cast("tuple[str, str]", self.tsdf.iloc[:, base_column].name)[
832
867
  0
833
868
  ]
@@ -837,7 +872,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
837
872
  if periods_in_a_year_fixed:
838
873
  time_factor = float(periods_in_a_year_fixed)
839
874
  else:
840
- time_factor = float(shortdf.count() / fraction)
875
+ time_factor = float(cast("int64", shortdf.count()) / fraction)
841
876
 
842
877
  terrors = []
843
878
  for item in self.tsdf:
@@ -848,10 +883,8 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
848
883
  :,
849
884
  item,
850
885
  ]
851
- relative = 1.0 + longdf - shortdf
852
- vol = float(
853
- relative.pct_change().std() * sqrt(time_factor),
854
- )
886
+ relative = longdf.ffill().pct_change() - shortdf.ffill().pct_change()
887
+ vol = float(relative.std() * sqrt(time_factor))
855
888
  terrors.append(vol)
856
889
 
857
890
  return Series(
@@ -897,7 +930,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
897
930
  Information Ratios
898
931
 
899
932
  """
900
- earlier, later = self.calc_range(months_from_last, from_date, to_date)
933
+ earlier, later = self.calc_range(
934
+ months_offset=months_from_last, from_dt=from_date, to_dt=to_date
935
+ )
901
936
  fraction = (later - earlier).days / 365.25
902
937
 
903
938
  msg = "base_column should be a tuple[str, ValueType] or an integer."
@@ -912,14 +947,20 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
912
947
  self.tsdf.loc[:, base_column].name,
913
948
  )[0]
914
949
  elif isinstance(base_column, int):
915
- shortdf = self.tsdf.loc[cast("int", earlier) : cast("int", later)].iloc[
916
- :,
917
- base_column,
918
- ]
919
- short_item = self.tsdf.iloc[
920
- :,
921
- base_column,
922
- ].name
950
+ shortdf = cast(
951
+ "DataFrame",
952
+ self.tsdf.loc[cast("int", earlier) : cast("int", later)].iloc[
953
+ :,
954
+ base_column,
955
+ ],
956
+ )
957
+ short_item = cast(
958
+ "tuple[str, ValueType]",
959
+ self.tsdf.iloc[
960
+ :,
961
+ base_column,
962
+ ].name,
963
+ )
923
964
  short_label = cast("tuple[str, str]", self.tsdf.iloc[:, base_column].name)[
924
965
  0
925
966
  ]
@@ -929,7 +970,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
929
970
  if periods_in_a_year_fixed:
930
971
  time_factor = float(periods_in_a_year_fixed)
931
972
  else:
932
- time_factor = float(shortdf.count() / fraction)
973
+ time_factor = float(shortdf.count() / fraction) # type: ignore[arg-type]
933
974
 
934
975
  ratios = []
935
976
  for item in self.tsdf:
@@ -940,13 +981,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
940
981
  :,
941
982
  item,
942
983
  ]
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
- )
984
+ relative = longdf.ffill().pct_change() - shortdf.ffill().pct_change()
985
+ ret = float(relative.mean() * time_factor)
986
+ vol = float(relative.std() * sqrt(time_factor))
950
987
  ratios.append(ret / vol)
951
988
 
952
989
  return Series(
@@ -1000,7 +1037,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1000
1037
 
1001
1038
  """
1002
1039
  loss_limit: float = 0.0
1003
- earlier, later = self.calc_range(months_from_last, from_date, to_date)
1040
+ earlier, later = self.calc_range(
1041
+ months_offset=months_from_last, from_dt=from_date, to_dt=to_date
1042
+ )
1004
1043
  fraction = (later - earlier).days / 365.25
1005
1044
 
1006
1045
  msg = "base_column should be a tuple[str, ValueType] or an integer."
@@ -1015,14 +1054,20 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1015
1054
  self.tsdf.loc[:, base_column].name,
1016
1055
  )[0]
1017
1056
  elif isinstance(base_column, int):
1018
- shortdf = self.tsdf.loc[cast("int", earlier) : cast("int", later)].iloc[
1019
- :,
1020
- base_column,
1021
- ]
1022
- short_item = self.tsdf.iloc[
1023
- :,
1024
- base_column,
1025
- ].name
1057
+ shortdf = cast(
1058
+ "DataFrame",
1059
+ self.tsdf.loc[cast("int", earlier) : cast("int", later)].iloc[
1060
+ :,
1061
+ base_column,
1062
+ ],
1063
+ )
1064
+ short_item = cast(
1065
+ "tuple[str, ValueType]",
1066
+ self.tsdf.iloc[
1067
+ :,
1068
+ base_column,
1069
+ ].name,
1070
+ )
1026
1071
  short_label = cast("tuple[str, str]", self.tsdf.iloc[:, base_column].name)[
1027
1072
  0
1028
1073
  ]
@@ -1032,7 +1077,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1032
1077
  if periods_in_a_year_fixed:
1033
1078
  time_factor = float(periods_in_a_year_fixed)
1034
1079
  else:
1035
- time_factor = float(shortdf.count() / fraction)
1080
+ time_factor = float(shortdf.count() / fraction) # type: ignore[arg-type]
1036
1081
 
1037
1082
  ratios = []
1038
1083
  for item in self.tsdf:
@@ -1046,16 +1091,18 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1046
1091
  msg = "ratio must be one of 'up', 'down' or 'both'."
1047
1092
  if ratio == "up":
1048
1093
  uparray = (
1049
- longdf.pct_change()[
1050
- shortdf.pct_change().to_numpy() > loss_limit
1094
+ longdf.ffill()
1095
+ .pct_change()[
1096
+ shortdf.ffill().pct_change().to_numpy() > loss_limit
1051
1097
  ]
1052
1098
  .add(1)
1053
1099
  .to_numpy()
1054
1100
  )
1055
1101
  up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
1056
1102
  upidxarray = (
1057
- shortdf.pct_change()[
1058
- shortdf.pct_change().to_numpy() > loss_limit
1103
+ shortdf.ffill()
1104
+ .pct_change()[
1105
+ shortdf.ffill().pct_change().to_numpy() > loss_limit
1059
1106
  ]
1060
1107
  .add(1)
1061
1108
  .to_numpy()
@@ -1066,8 +1113,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1066
1113
  ratios.append(up_rtrn / up_idx_return)
1067
1114
  elif ratio == "down":
1068
1115
  downarray = (
1069
- longdf.pct_change()[
1070
- shortdf.pct_change().to_numpy() < loss_limit
1116
+ longdf.ffill()
1117
+ .pct_change()[
1118
+ shortdf.ffill().pct_change().to_numpy() < loss_limit
1071
1119
  ]
1072
1120
  .add(1)
1073
1121
  .to_numpy()
@@ -1076,8 +1124,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1076
1124
  downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
1077
1125
  )
1078
1126
  downidxarray = (
1079
- shortdf.pct_change()[
1080
- shortdf.pct_change().to_numpy() < loss_limit
1127
+ shortdf.ffill()
1128
+ .pct_change()[
1129
+ shortdf.ffill().pct_change().to_numpy() < loss_limit
1081
1130
  ]
1082
1131
  .add(1)
1083
1132
  .to_numpy()
@@ -1089,16 +1138,18 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1089
1138
  ratios.append(down_return / down_idx_return)
1090
1139
  elif ratio == "both":
1091
1140
  uparray = (
1092
- longdf.pct_change()[
1093
- shortdf.pct_change().to_numpy() > loss_limit
1141
+ longdf.ffill()
1142
+ .pct_change()[
1143
+ shortdf.ffill().pct_change().to_numpy() > loss_limit
1094
1144
  ]
1095
1145
  .add(1)
1096
1146
  .to_numpy()
1097
1147
  )
1098
1148
  up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
1099
1149
  upidxarray = (
1100
- shortdf.pct_change()[
1101
- shortdf.pct_change().to_numpy() > loss_limit
1150
+ shortdf.ffill()
1151
+ .pct_change()[
1152
+ shortdf.ffill().pct_change().to_numpy() > loss_limit
1102
1153
  ]
1103
1154
  .add(1)
1104
1155
  .to_numpy()
@@ -1107,8 +1158,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1107
1158
  upidxarray.prod() ** (1 / (len(upidxarray) / time_factor)) - 1
1108
1159
  )
1109
1160
  downarray = (
1110
- longdf.pct_change()[
1111
- shortdf.pct_change().to_numpy() < loss_limit
1161
+ longdf.ffill()
1162
+ .pct_change()[
1163
+ shortdf.ffill().pct_change().to_numpy() < loss_limit
1112
1164
  ]
1113
1165
  .add(1)
1114
1166
  .to_numpy()
@@ -1117,8 +1169,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1117
1169
  downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
1118
1170
  )
1119
1171
  downidxarray = (
1120
- shortdf.pct_change()[
1121
- shortdf.pct_change().to_numpy() < loss_limit
1172
+ shortdf.ffill()
1173
+ .pct_change()[
1174
+ shortdf.ffill().pct_change().to_numpy() < loss_limit
1122
1175
  ]
1123
1176
  .add(1)
1124
1177
  .to_numpy()
@@ -1173,15 +1226,13 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1173
1226
  Beta as Co-variance of x & y divided by Variance of x
1174
1227
 
1175
1228
  """
1176
- if all(
1177
- x_value == ValueType.RTRN
1178
- for x_value in self.tsdf.columns.get_level_values(1).to_numpy()
1179
- ):
1229
+ vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
1230
+ if all(vtypes):
1180
1231
  msg = "asset should be a tuple[str, ValueType] or an integer."
1181
1232
  if isinstance(asset, tuple):
1182
1233
  y_value = self.tsdf.loc[:, asset]
1183
1234
  elif isinstance(asset, int):
1184
- y_value = self.tsdf.iloc[:, asset]
1235
+ y_value = cast("DataFrame", self.tsdf.iloc[:, asset])
1185
1236
  else:
1186
1237
  raise TypeError(msg)
1187
1238
 
@@ -1189,36 +1240,35 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1189
1240
  if isinstance(market, tuple):
1190
1241
  x_value = self.tsdf.loc[:, market]
1191
1242
  elif isinstance(market, int):
1192
- x_value = self.tsdf.iloc[:, market]
1243
+ x_value = cast("DataFrame", self.tsdf.iloc[:, market])
1193
1244
  else:
1194
1245
  raise TypeError(msg)
1195
- else:
1246
+ elif not any(vtypes):
1196
1247
  msg = "asset should be a tuple[str, ValueType] or an integer."
1197
1248
  if isinstance(asset, tuple):
1198
- y_value = log(
1199
- self.tsdf.loc[:, asset] / self.tsdf.loc[:, asset].iloc[0],
1200
- )
1249
+ y_value = self.tsdf.loc[:, asset].ffill().pct_change().iloc[1:]
1201
1250
  elif isinstance(asset, int):
1202
- y_value = log(
1203
- self.tsdf.iloc[:, asset] / cast("float", self.tsdf.iloc[0, asset]),
1251
+ y_value = cast(
1252
+ "DataFrame", self.tsdf.iloc[:, asset].ffill().pct_change().iloc[1:]
1204
1253
  )
1205
1254
  else:
1206
1255
  raise TypeError(msg)
1207
-
1208
1256
  msg = "market should be a tuple[str, ValueType] or an integer."
1257
+
1209
1258
  if isinstance(market, tuple):
1210
- x_value = log(
1211
- self.tsdf.loc[:, market] / self.tsdf.loc[:, market].iloc[0],
1212
- )
1259
+ x_value = self.tsdf.loc[:, market].ffill().pct_change().iloc[1:]
1213
1260
  elif isinstance(market, int):
1214
- x_value = log(
1215
- self.tsdf.iloc[:, market]
1216
- / cast("float", self.tsdf.iloc[0, market]),
1261
+ x_value = cast(
1262
+ "DataFrame",
1263
+ self.tsdf.iloc[:, market].ffill().pct_change().iloc[1:],
1217
1264
  )
1218
1265
  else:
1219
1266
  raise TypeError(msg)
1267
+ else:
1268
+ msg = "Mix of series types will give inconsistent results"
1269
+ raise MixedValuetypesError(msg)
1220
1270
 
1221
- covariance = cov(y_value, x_value, ddof=dlta_degr_freedms)
1271
+ covariance = cov(m=y_value, y=x_value, ddof=dlta_degr_freedms)
1222
1272
  beta = covariance[0, 1] / covariance[1, 1]
1223
1273
 
1224
1274
  return float(beta)
@@ -1258,7 +1308,10 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1258
1308
  self.tsdf.loc[:, y_column].name,
1259
1309
  )[0]
1260
1310
  elif isinstance(y_column, int):
1261
- y_value = self.tsdf.iloc[:, y_column].to_numpy()
1311
+ y_value = cast(
1312
+ "ndarray[tuple[int, int], dtype[Any]]",
1313
+ self.tsdf.iloc[:, y_column].to_numpy(),
1314
+ )
1262
1315
  y_label = cast("tuple[str, str]", self.tsdf.iloc[:, y_column].name)[0]
1263
1316
  else:
1264
1317
  raise TypeError(msg)
@@ -1319,105 +1372,62 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1319
1372
  Jensen's alpha
1320
1373
 
1321
1374
  """
1322
- full_year = 1.0
1323
1375
  vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
1324
1376
  if not any(vtypes):
1325
1377
  msg = "asset should be a tuple[str, ValueType] or an integer."
1326
1378
  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
- )
1379
+ asset_rtn = self.tsdf.loc[:, asset].ffill().pct_change().iloc[1:]
1380
+ asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
1341
1381
  elif isinstance(asset, int):
1342
- asset_log = log(
1343
- self.tsdf.iloc[:, asset] / cast("float", self.tsdf.iloc[0, asset]),
1382
+ asset_rtn = cast(
1383
+ "DataFrame", self.tsdf.iloc[:, asset].ffill().pct_change().iloc[1:]
1344
1384
  )
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
- )
1385
+ asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
1356
1386
  else:
1357
1387
  raise TypeError(msg)
1358
1388
 
1359
1389
  msg = "market should be a tuple[str, ValueType] or an integer."
1360
1390
  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
- )
1391
+ market_rtn = self.tsdf.loc[:, market].ffill().pct_change().iloc[1:]
1392
+ market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
1375
1393
  elif isinstance(market, int):
1376
- market_log = log(
1377
- self.tsdf.iloc[:, market]
1378
- / cast("float", self.tsdf.iloc[0, market]),
1394
+ market_rtn = cast(
1395
+ "DataFrame",
1396
+ self.tsdf.iloc[:, market].ffill().pct_change().iloc[1:],
1379
1397
  )
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
- )
1398
+ market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
1391
1399
  else:
1392
1400
  raise TypeError(msg)
1393
1401
  elif all(vtypes):
1394
1402
  msg = "asset should be a tuple[str, ValueType] or an integer."
1395
1403
  if isinstance(asset, tuple):
1396
- asset_log = self.tsdf.loc[:, asset]
1397
- asset_cagr = asset_log.mean()
1404
+ asset_rtn = self.tsdf.loc[:, asset]
1405
+ asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
1398
1406
  elif isinstance(asset, int):
1399
- asset_log = self.tsdf.iloc[:, asset]
1400
- asset_cagr = asset_log.mean()
1407
+ asset_rtn = cast("DataFrame", self.tsdf.iloc[:, asset])
1408
+ asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
1401
1409
  else:
1402
1410
  raise TypeError(msg)
1403
1411
 
1404
1412
  msg = "market should be a tuple[str, ValueType] or an integer."
1405
1413
  if isinstance(market, tuple):
1406
- market_log = self.tsdf.loc[:, market]
1407
- market_cagr = market_log.mean()
1414
+ market_rtn = self.tsdf.loc[:, market]
1415
+ market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
1408
1416
  elif isinstance(market, int):
1409
- market_log = self.tsdf.iloc[:, market]
1410
- market_cagr = market_log.mean()
1417
+ market_rtn = cast("DataFrame", self.tsdf.iloc[:, market])
1418
+ market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
1411
1419
  else:
1412
1420
  raise TypeError(msg)
1413
1421
  else:
1414
1422
  msg = "Mix of series types will give inconsistent results"
1415
1423
  raise MixedValuetypesError(msg)
1416
1424
 
1417
- covariance = cov(m=asset_log, y=market_log, ddof=dlta_degr_freedms)
1425
+ covariance = cov(m=asset_rtn, y=market_rtn, ddof=dlta_degr_freedms)
1418
1426
  beta = covariance[0, 1] / covariance[1, 1]
1419
1427
 
1420
- return float(asset_cagr - riskfree_rate - beta * (market_cagr - riskfree_rate))
1428
+ return float(
1429
+ asset_rtn_mean - riskfree_rate - beta * (market_rtn_mean - riskfree_rate)
1430
+ )
1421
1431
 
1422
1432
  def make_portfolio(
1423
1433
  self: Self,
@@ -1448,7 +1458,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1448
1458
 
1449
1459
  vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
1450
1460
  if not any(vtypes):
1451
- returns = self.tsdf.pct_change()
1461
+ returns = self.tsdf.ffill().pct_change()
1452
1462
  returns.iloc[0] = 0
1453
1463
  elif all(vtypes):
1454
1464
  returns = self.tsdf.copy()
@@ -1523,11 +1533,14 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1523
1533
  )
1524
1534
 
1525
1535
  retseries = (
1526
- relative.pct_change().rolling(observations, min_periods=observations).sum()
1536
+ relative.ffill()
1537
+ .pct_change()
1538
+ .rolling(observations, min_periods=observations)
1539
+ .sum()
1527
1540
  )
1528
1541
  retdf = retseries.dropna().to_frame()
1529
1542
 
1530
- voldf = relative.pct_change().rolling(
1543
+ voldf = relative.ffill().pct_change().rolling(
1531
1544
  observations,
1532
1545
  min_periods=observations,
1533
1546
  ).std() * sqrt(time_factor)
@@ -1573,9 +1586,13 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1573
1586
  asset_label = cast("tuple[str, str]", self.tsdf.iloc[:, asset_column].name)[0]
1574
1587
  beta_label = f"{asset_label} / {market_label}"
1575
1588
 
1576
- rolling = self.tsdf.pct_change().rolling(
1577
- observations,
1578
- min_periods=observations,
1589
+ rolling = (
1590
+ self.tsdf.ffill()
1591
+ .pct_change()
1592
+ .rolling(
1593
+ observations,
1594
+ min_periods=observations,
1595
+ )
1579
1596
  )
1580
1597
 
1581
1598
  rcov = rolling.cov(ddof=dlta_degr_freedms)
@@ -1595,7 +1612,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1595
1612
  rollbeta.index = rollbeta.index.droplevel(level=1)
1596
1613
  rollbeta.columns = MultiIndex.from_arrays([[beta_label], ["Beta"]])
1597
1614
 
1598
- return rollbeta
1615
+ return cast("DataFrame", rollbeta)
1599
1616
 
1600
1617
  def rolling_corr(
1601
1618
  self: Self,
@@ -1630,10 +1647,11 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1630
1647
  )
1631
1648
  first_series = (
1632
1649
  self.tsdf.iloc[:, first_column]
1650
+ .ffill()
1633
1651
  .pct_change()[1:]
1634
1652
  .rolling(observations, min_periods=observations)
1635
1653
  )
1636
- second_series = self.tsdf.iloc[:, second_column].pct_change()[1:]
1654
+ second_series = self.tsdf.iloc[:, second_column].ffill().pct_change()[1:]
1637
1655
  corrdf = first_series.corr(other=second_series).dropna().to_frame()
1638
1656
  corrdf.columns = MultiIndex.from_arrays(
1639
1657
  [