openseries 1.7.0__py3-none-any.whl → 1.7.2__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/__init__.py CHANGED
@@ -3,7 +3,6 @@
3
3
  from .datefixer import (
4
4
  date_fix,
5
5
  date_offset_foll,
6
- do_resample_to_business_period_ends,
7
6
  generate_calendar_date_range,
8
7
  get_previous_business_day_before_today,
9
8
  holiday_calendar,
@@ -31,7 +30,6 @@ __all__ = [
31
30
  "simulate_portfolios",
32
31
  "date_fix",
33
32
  "date_offset_foll",
34
- "do_resample_to_business_period_ends",
35
33
  "generate_calendar_date_range",
36
34
  "get_previous_business_day_before_today",
37
35
  "holiday_calendar",
@@ -447,7 +447,7 @@ class _CommonModel(BaseModel):
447
447
  interpolation: LiteralQuantileInterp = "lower"
448
448
  return self.vol_from_var_func(level=level, interpolation=interpolation)
449
449
 
450
- def calc_range( # noqa: C901
450
+ def calc_range(
451
451
  self: Self,
452
452
  months_offset: int | None = None,
453
453
  from_dt: dt.date | None = None,
@@ -471,42 +471,36 @@ class _CommonModel(BaseModel):
471
471
  Start and end date of the chosen date range
472
472
 
473
473
  """
474
- earlier, later = self.tsdf.index[0], self.tsdf.index[-1]
475
- if any([months_offset, from_dt, to_dt]):
476
- if months_offset is not None:
477
- earlier = date_offset_foll(
478
- raw_date=DatetimeIndex(self.tsdf.index)[-1],
479
- months_offset=-months_offset,
480
- adjust=False,
481
- following=True,
474
+ earlier, later = self.first_idx, self.last_idx
475
+ if months_offset is not None:
476
+ earlier = date_offset_foll(
477
+ raw_date=self.last_idx,
478
+ months_offset=-months_offset,
479
+ adjust=False,
480
+ following=True,
481
+ )
482
+ if earlier < self.first_idx:
483
+ msg = (
484
+ "Argument months_offset implies start"
485
+ "date before first date in series."
482
486
  )
483
- if earlier < self.tsdf.index[0]:
484
- msg = "Function calc_range returned earlier date < series start"
485
- raise ValueError(
486
- msg,
487
- )
488
- later = self.tsdf.index[-1]
489
- elif from_dt is not None and to_dt is None:
490
- if from_dt < self.tsdf.index[0]:
487
+ raise ValueError(msg)
488
+ later = self.last_idx
489
+ else:
490
+ if from_dt is not None:
491
+ if from_dt < self.first_idx:
491
492
  msg = "Given from_dt date < series start"
492
493
  raise ValueError(msg)
493
- earlier, later = from_dt, self.tsdf.index[-1]
494
- elif from_dt is None and to_dt is not None:
495
- if to_dt > self.tsdf.index[-1]:
494
+ earlier = from_dt
495
+ if to_dt is not None:
496
+ if to_dt > self.last_idx:
496
497
  msg = "Given to_dt date > series end"
497
498
  raise ValueError(msg)
498
- earlier, later = self.tsdf.index[0], to_dt
499
- elif from_dt is not None and to_dt is not None:
500
- if to_dt > self.tsdf.index[-1] or from_dt < self.tsdf.index[0]:
501
- msg = "Given from_dt or to_dt dates outside series range"
502
- raise ValueError(
503
- msg,
504
- )
505
- earlier, later = from_dt, to_dt
506
- while earlier not in self.tsdf.index.tolist():
507
- earlier -= dt.timedelta(days=1)
508
- while later not in self.tsdf.index.tolist():
509
- later += dt.timedelta(days=1)
499
+ later = to_dt
500
+ while earlier not in self.tsdf.index:
501
+ earlier -= dt.timedelta(days=1)
502
+ while later not in self.tsdf.index:
503
+ later += dt.timedelta(days=1)
510
504
 
511
505
  return earlier, later
512
506
 
@@ -789,9 +783,7 @@ class _CommonModel(BaseModel):
789
783
  if labels:
790
784
  if len(labels) != self.tsdf.shape[1]:
791
785
  msg = "Must provide same number of labels as items in frame."
792
- raise ValueError(
793
- msg,
794
- )
786
+ raise ValueError(msg)
795
787
  else:
796
788
  labels = list(self.tsdf.columns.get_level_values(0))
797
789
 
@@ -906,9 +898,7 @@ class _CommonModel(BaseModel):
906
898
  if labels:
907
899
  if len(labels) != self.tsdf.shape[1]:
908
900
  msg = "Must provide same number of labels as items in frame."
909
- raise ValueError(
910
- msg,
911
- )
901
+ raise ValueError(msg)
912
902
  else:
913
903
  labels = list(self.tsdf.columns.get_level_values(0))
914
904
 
@@ -1936,9 +1926,7 @@ class _CommonModel(BaseModel):
1936
1926
  "Simple return cannot be calculated due to "
1937
1927
  f"an initial value being zero. ({self.tsdf.head(3)})"
1938
1928
  )
1939
- raise ValueError(
1940
- msg,
1941
- )
1929
+ raise ValueError(msg)
1942
1930
 
1943
1931
  result = self.tsdf.loc[later] / self.tsdf.loc[earlier] - 1
1944
1932
 
openseries/datefixer.py CHANGED
@@ -15,15 +15,14 @@ from pandas import (
15
15
  DataFrame,
16
16
  DatetimeIndex,
17
17
  Index,
18
- Series,
19
18
  Timestamp,
20
19
  concat,
21
20
  date_range,
22
21
  )
23
22
  from pandas.tseries.offsets import CustomBusinessDay
24
23
 
25
- if TYPE_CHECKING: # pragma: no cover
26
- from .types import (
24
+ if TYPE_CHECKING:
25
+ from .types import ( # pragma: no cover
27
26
  CountriesType,
28
27
  DateType,
29
28
  HolidayType,
@@ -33,7 +32,6 @@ if TYPE_CHECKING: # pragma: no cover
33
32
  __all__ = [
34
33
  "date_fix",
35
34
  "date_offset_foll",
36
- "do_resample_to_business_period_ends",
37
35
  "generate_calendar_date_range",
38
36
  "get_previous_business_day_before_today",
39
37
  "holiday_calendar",
@@ -94,9 +92,7 @@ def holiday_calendar(
94
92
  "Argument countries must be a string country code or "
95
93
  "a list of string country codes according to ISO 3166-1 alpha-2."
96
94
  )
97
- raise ValueError(
98
- msg,
99
- )
95
+ raise ValueError(msg)
100
96
 
101
97
  return busdaycalendar(holidays=hols)
102
98
 
@@ -117,6 +113,7 @@ def date_fix(
117
113
  Parsed date
118
114
 
119
115
  """
116
+ msg = f"Unknown date format {fixerdate!s} of type {type(fixerdate)!s} encountered"
120
117
  if isinstance(fixerdate, (Timestamp, dt.datetime)):
121
118
  return fixerdate.date()
122
119
  if isinstance(fixerdate, dt.date):
@@ -127,10 +124,7 @@ def date_fix(
127
124
  )
128
125
  if isinstance(fixerdate, str):
129
126
  return dt.datetime.strptime(fixerdate, "%Y-%m-%d").astimezone().date()
130
- msg = f"Unknown date format {fixerdate!s} of type {type(fixerdate)!s} encountered"
131
- raise TypeError(
132
- msg,
133
- )
127
+ raise TypeError(msg)
134
128
 
135
129
 
136
130
  def date_offset_foll(
@@ -374,15 +368,12 @@ def generate_calendar_date_range(
374
368
  "Provide one of start or end date, but not both. "
375
369
  "Date range is inferred from number of trading days."
376
370
  )
377
- raise ValueError(
378
- msg,
379
- )
371
+ raise ValueError(msg)
380
372
 
381
373
 
382
- def do_resample_to_business_period_ends(
374
+ # noinspection PyUnusedLocal
375
+ def _do_resample_to_business_period_ends(
383
376
  data: DataFrame,
384
- head: Series[float],
385
- tail: Series[float],
386
377
  freq: LiteralBizDayFreq,
387
378
  countries: CountriesType,
388
379
  ) -> DatetimeIndex:
@@ -394,10 +385,6 @@ def do_resample_to_business_period_ends(
394
385
  ----------
395
386
  data: pandas.DataFrame
396
387
  The timeseries data
397
- head: pandas:Series[float]
398
- Data point at maximum first date of all series
399
- tail: pandas:Series[float]
400
- Data point at minimum last date of all series
401
388
  freq: LiteralBizDayFreq
402
389
  The date offset string that sets the resampled frequency
403
390
  countries: CountriesType
@@ -410,25 +397,16 @@ def do_resample_to_business_period_ends(
410
397
  A date range aligned to business period ends
411
398
 
412
399
  """
413
- newhead = head.to_frame().T
414
- newtail = tail.to_frame().T
415
- data.index = DatetimeIndex(data.index)
416
- data = data.resample(rule=freq).last()
417
- data = data.drop(index=data.index[-1])
418
- data.index = Index(d.date() for d in DatetimeIndex(data.index))
419
-
420
- if newhead.index[0] not in data.index:
421
- # noinspection PyUnreachableCode
422
- data = concat([data, newhead])
423
-
424
- if newtail.index[0] not in data.index:
425
- # noinspection PyUnreachableCode
426
- data = concat([data, newtail])
400
+ copydata = data.copy()
401
+ copydata.index = DatetimeIndex(copydata.index)
402
+ copydata = copydata.resample(rule=freq).last()
403
+ copydata = copydata.drop(index=copydata.index[-1])
404
+ copydata.index = Index(d.date() for d in DatetimeIndex(copydata.index))
427
405
 
428
- data = data.sort_index()
406
+ copydata = concat([data.head(n=1), copydata, data.tail(n=1)]).sort_index()
429
407
 
430
408
  dates = DatetimeIndex(
431
- [data.index[0]]
409
+ [copydata.index[0]]
432
410
  + [
433
411
  date_offset_foll(
434
412
  dt.date(d.year, d.month, 1)
@@ -439,8 +417,8 @@ def do_resample_to_business_period_ends(
439
417
  adjust=True,
440
418
  following=False,
441
419
  )
442
- for d in data.index[1:-1]
420
+ for d in copydata.index[1:-1]
443
421
  ]
444
- + [data.index[-1]],
422
+ + [copydata.index[-1]],
445
423
  )
446
424
  return dates.drop_duplicates()
openseries/frame.py CHANGED
@@ -13,7 +13,6 @@ if TYPE_CHECKING:
13
13
 
14
14
  import statsmodels.api as sm # type: ignore[import-untyped,unused-ignore]
15
15
  from numpy import (
16
- array,
17
16
  cov,
18
17
  cumprod,
19
18
  divide,
@@ -43,7 +42,7 @@ from statsmodels.regression.linear_model import ( # type: ignore[import-untyped
43
42
  from typing_extensions import Self
44
43
 
45
44
  from ._common_model import _CommonModel
46
- from .datefixer import do_resample_to_business_period_ends
45
+ from .datefixer import _do_resample_to_business_period_ends
47
46
  from .series import OpenTimeSeries
48
47
  from .types import (
49
48
  CountriesType,
@@ -443,21 +442,18 @@ class OpenFrame(_CommonModel):
443
442
  An OpenFrame object
444
443
 
445
444
  """
446
- head: Series[float] = self.tsdf.loc[self.first_indices.max()].copy()
447
- tail: Series[float] = self.tsdf.loc[self.last_indices.min()].copy()
448
- dates = do_resample_to_business_period_ends(
449
- data=self.tsdf,
450
- head=head,
451
- tail=tail,
452
- freq=freq,
453
- countries=countries,
454
- )
455
- self.tsdf = self.tsdf.reindex([deyt.date() for deyt in dates], method=method)
456
445
  for xerie in self.constituents:
446
+ dates = _do_resample_to_business_period_ends(
447
+ data=xerie.tsdf,
448
+ freq=freq,
449
+ countries=countries,
450
+ )
457
451
  xerie.tsdf = xerie.tsdf.reindex(
458
- [deyt.date() for deyt in dates],
459
- method=method,
452
+ [deyt.date() for deyt in dates], method=method,
460
453
  )
454
+
455
+ self._set_tsdf()
456
+
461
457
  return self
462
458
 
463
459
  def ewma_risk(
@@ -780,6 +776,7 @@ class OpenFrame(_CommonModel):
780
776
  earlier, later = self.calc_range(months_from_last, from_date, to_date)
781
777
  fraction = (later - earlier).days / 365.25
782
778
 
779
+ msg = "base_column should be a tuple[str, ValueType] or an integer."
783
780
  if isinstance(base_column, tuple):
784
781
  shortdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].loc[
785
782
  :,
@@ -801,10 +798,7 @@ class OpenFrame(_CommonModel):
801
798
  ].name
802
799
  short_label = cast(tuple[str, str], self.tsdf.iloc[:, base_column].name)[0]
803
800
  else:
804
- msg = "base_column should be a tuple[str, ValueType] or an integer."
805
- raise TypeError(
806
- msg,
807
- )
801
+ raise TypeError(msg)
808
802
 
809
803
  if periods_in_a_year_fixed:
810
804
  time_factor = float(periods_in_a_year_fixed)
@@ -873,6 +867,7 @@ class OpenFrame(_CommonModel):
873
867
  earlier, later = self.calc_range(months_from_last, from_date, to_date)
874
868
  fraction = (later - earlier).days / 365.25
875
869
 
870
+ msg = "base_column should be a tuple[str, ValueType] or an integer."
876
871
  if isinstance(base_column, tuple):
877
872
  shortdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].loc[
878
873
  :,
@@ -894,10 +889,7 @@ class OpenFrame(_CommonModel):
894
889
  ].name
895
890
  short_label = cast(tuple[str, str], self.tsdf.iloc[:, base_column].name)[0]
896
891
  else:
897
- msg = "base_column should be a tuple[str, ValueType] or an integer."
898
- raise TypeError(
899
- msg,
900
- )
892
+ raise TypeError(msg)
901
893
 
902
894
  if periods_in_a_year_fixed:
903
895
  time_factor = float(periods_in_a_year_fixed)
@@ -977,6 +969,7 @@ class OpenFrame(_CommonModel):
977
969
  earlier, later = self.calc_range(months_from_last, from_date, to_date)
978
970
  fraction = (later - earlier).days / 365.25
979
971
 
972
+ msg = "base_column should be a tuple[str, ValueType] or an integer."
980
973
  if isinstance(base_column, tuple):
981
974
  shortdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].loc[
982
975
  :,
@@ -998,10 +991,7 @@ class OpenFrame(_CommonModel):
998
991
  ].name
999
992
  short_label = cast(tuple[str, str], self.tsdf.iloc[:, base_column].name)[0]
1000
993
  else:
1001
- msg = "base_column should be a tuple[str, ValueType] or an integer."
1002
- raise TypeError(
1003
- msg,
1004
- )
994
+ raise TypeError(msg)
1005
995
 
1006
996
  if periods_in_a_year_fixed:
1007
997
  time_factor = float(periods_in_a_year_fixed)
@@ -1017,6 +1007,7 @@ class OpenFrame(_CommonModel):
1017
1007
  :,
1018
1008
  item,
1019
1009
  ]
1010
+ msg = "ratio must be one of 'up', 'down' or 'both'."
1020
1011
  if ratio == "up":
1021
1012
  uparray = (
1022
1013
  longdf.pct_change(fill_method=None)[
@@ -1111,6 +1102,8 @@ class OpenFrame(_CommonModel):
1111
1102
  ratios.append(
1112
1103
  (up_rtrn / up_idx_return) / (down_return / down_idx_return),
1113
1104
  )
1105
+ else:
1106
+ raise ValueError(msg)
1114
1107
 
1115
1108
  if ratio == "up":
1116
1109
  resultname = f"Up Capture Ratios vs {short_label}"
@@ -1156,25 +1149,23 @@ class OpenFrame(_CommonModel):
1156
1149
  x_value == ValueType.RTRN
1157
1150
  for x_value in self.tsdf.columns.get_level_values(1).to_numpy()
1158
1151
  ):
1152
+ msg = "asset should be a tuple[str, ValueType] or an integer."
1159
1153
  if isinstance(asset, tuple):
1160
1154
  y_value = self.tsdf.loc[:, asset]
1161
1155
  elif isinstance(asset, int):
1162
1156
  y_value = self.tsdf.iloc[:, asset]
1163
1157
  else:
1164
- msg = "asset should be a tuple[str, ValueType] or an integer."
1165
- raise TypeError(
1166
- msg,
1167
- )
1158
+ raise TypeError(msg)
1159
+
1160
+ msg = "market should be a tuple[str, ValueType] or an integer."
1168
1161
  if isinstance(market, tuple):
1169
1162
  x_value = self.tsdf.loc[:, market]
1170
1163
  elif isinstance(market, int):
1171
1164
  x_value = self.tsdf.iloc[:, market]
1172
1165
  else:
1173
- msg = "market should be a tuple[str, ValueType] or an integer."
1174
- raise TypeError(
1175
- msg,
1176
- )
1166
+ raise TypeError(msg)
1177
1167
  else:
1168
+ msg = "asset should be a tuple[str, ValueType] or an integer."
1178
1169
  if isinstance(asset, tuple):
1179
1170
  y_value = log(
1180
1171
  self.tsdf.loc[:, asset] / self.tsdf.loc[:, asset].iloc[0],
@@ -1184,10 +1175,9 @@ class OpenFrame(_CommonModel):
1184
1175
  self.tsdf.iloc[:, asset] / cast(float, self.tsdf.iloc[0, asset]),
1185
1176
  )
1186
1177
  else:
1187
- msg = "asset should be a tuple[str, ValueType] or an integer."
1188
- raise TypeError(
1189
- msg,
1190
- )
1178
+ raise TypeError(msg)
1179
+
1180
+ msg = "market should be a tuple[str, ValueType] or an integer."
1191
1181
  if isinstance(market, tuple):
1192
1182
  x_value = log(
1193
1183
  self.tsdf.loc[:, market] / self.tsdf.loc[:, market].iloc[0],
@@ -1197,10 +1187,7 @@ class OpenFrame(_CommonModel):
1197
1187
  self.tsdf.iloc[:, market] / cast(float, self.tsdf.iloc[0, market]),
1198
1188
  )
1199
1189
  else:
1200
- msg = "market should be a tuple[str, ValueType] or an integer."
1201
- raise TypeError(
1202
- msg,
1203
- )
1190
+ raise TypeError(msg)
1204
1191
 
1205
1192
  covariance = cov(y_value, x_value, ddof=dlta_degr_freedms)
1206
1193
  beta = covariance[0, 1] / covariance[1, 1]
@@ -1241,6 +1228,7 @@ class OpenFrame(_CommonModel):
1241
1228
  The Statsmodels regression output
1242
1229
 
1243
1230
  """
1231
+ msg = "y_column should be a tuple[str, ValueType] or an integer."
1244
1232
  if isinstance(y_column, tuple):
1245
1233
  y_value = self.tsdf.loc[:, y_column]
1246
1234
  y_label = cast(
@@ -1251,11 +1239,9 @@ class OpenFrame(_CommonModel):
1251
1239
  y_value = self.tsdf.iloc[:, y_column]
1252
1240
  y_label = cast(tuple[str, str], self.tsdf.iloc[:, y_column].name)[0]
1253
1241
  else:
1254
- msg = "y_column should be a tuple[str, ValueType] or an integer."
1255
- raise TypeError(
1256
- msg,
1257
- )
1242
+ raise TypeError(msg)
1258
1243
 
1244
+ msg = "x_column should be a tuple[str, ValueType] or an integer."
1259
1245
  if isinstance(x_column, tuple):
1260
1246
  x_value = self.tsdf.loc[:, x_column]
1261
1247
  x_label = cast(
@@ -1266,10 +1252,7 @@ class OpenFrame(_CommonModel):
1266
1252
  x_value = self.tsdf.iloc[:, x_column]
1267
1253
  x_label = cast(tuple[str, str], self.tsdf.iloc[:, x_column].name)[0]
1268
1254
  else:
1269
- msg = "x_column should be a tuple[str, ValueType] or an integer."
1270
- raise TypeError(
1271
- msg,
1272
- )
1255
+ raise TypeError(msg)
1273
1256
 
1274
1257
  results = sm.OLS(y_value, x_value).fit(method=method, cov_type=cov_type)
1275
1258
  if fitted_series:
@@ -1315,6 +1298,7 @@ class OpenFrame(_CommonModel):
1315
1298
  x == ValueType.RTRN
1316
1299
  for x in self.tsdf.columns.get_level_values(1).to_numpy()
1317
1300
  ):
1301
+ msg = "asset should be a tuple[str, ValueType] or an integer."
1318
1302
  if isinstance(asset, tuple):
1319
1303
  asset_log = self.tsdf.loc[:, asset]
1320
1304
  asset_cagr = asset_log.mean()
@@ -1322,10 +1306,9 @@ class OpenFrame(_CommonModel):
1322
1306
  asset_log = self.tsdf.iloc[:, asset]
1323
1307
  asset_cagr = asset_log.mean()
1324
1308
  else:
1325
- msg = "asset should be a tuple[str, ValueType] or an integer."
1326
- raise TypeError(
1327
- msg,
1328
- )
1309
+ raise TypeError(msg)
1310
+
1311
+ msg = "market should be a tuple[str, ValueType] or an integer."
1329
1312
  if isinstance(market, tuple):
1330
1313
  market_log = self.tsdf.loc[:, market]
1331
1314
  market_cagr = market_log.mean()
@@ -1333,11 +1316,9 @@ class OpenFrame(_CommonModel):
1333
1316
  market_log = self.tsdf.iloc[:, market]
1334
1317
  market_cagr = market_log.mean()
1335
1318
  else:
1336
- msg = "market should be a tuple[str, ValueType] or an integer."
1337
- raise TypeError(
1338
- msg,
1339
- )
1319
+ raise TypeError(msg)
1340
1320
  else:
1321
+ msg = "asset should be a tuple[str, ValueType] or an integer."
1341
1322
  if isinstance(asset, tuple):
1342
1323
  asset_log = log(
1343
1324
  self.tsdf.loc[:, asset] / self.tsdf.loc[:, asset].iloc[0],
@@ -1369,10 +1350,9 @@ class OpenFrame(_CommonModel):
1369
1350
  - 1
1370
1351
  )
1371
1352
  else:
1372
- msg = "asset should be a tuple[str, ValueType] or an integer."
1373
- raise TypeError(
1374
- msg,
1375
- )
1353
+ raise TypeError(msg)
1354
+
1355
+ msg = "market should be a tuple[str, ValueType] or an integer."
1376
1356
  if isinstance(market, tuple):
1377
1357
  market_log = log(
1378
1358
  self.tsdf.loc[:, market] / self.tsdf.loc[:, market].iloc[0],
@@ -1404,10 +1384,7 @@ class OpenFrame(_CommonModel):
1404
1384
  - 1
1405
1385
  )
1406
1386
  else:
1407
- msg = "market should be a tuple[str, ValueType] or an integer."
1408
- raise TypeError(
1409
- msg,
1410
- )
1387
+ raise TypeError(msg)
1411
1388
 
1412
1389
  covariance = cov(asset_log, market_log, ddof=dlta_degr_freedms)
1413
1390
  beta = covariance[0, 1] / covariance[1, 1]
@@ -1439,9 +1416,7 @@ class OpenFrame(_CommonModel):
1439
1416
  "OpenFrame weights property must be provided "
1440
1417
  "to run the make_portfolio method."
1441
1418
  )
1442
- raise ValueError(
1443
- msg,
1444
- )
1419
+ raise ValueError(msg)
1445
1420
  dframe = self.tsdf.copy()
1446
1421
  if not any(
1447
1422
  x == ValueType.RTRN
@@ -1449,6 +1424,8 @@ class OpenFrame(_CommonModel):
1449
1424
  ):
1450
1425
  dframe = dframe.pct_change(fill_method=None)
1451
1426
  dframe.iloc[0] = 0
1427
+
1428
+ msg = "Weight strategy not implemented"
1452
1429
  if weight_strat:
1453
1430
  if weight_strat == "eq_weights":
1454
1431
  self.weights = [1.0 / self.item_count] * self.item_count
@@ -1457,10 +1434,10 @@ class OpenFrame(_CommonModel):
1457
1434
  vol[isinf(vol)] = nan
1458
1435
  self.weights = list(divide(vol, vol.sum()))
1459
1436
  else:
1460
- msg = "Weight strategy not implemented"
1461
1437
  raise NotImplementedError(msg)
1438
+
1462
1439
  return DataFrame(
1463
- data=dframe.dot(other=array(self.weights)).add(1.0).cumprod(),
1440
+ data=(dframe @ self.weights).add(1.0).cumprod(),
1464
1441
  index=self.tsdf.index,
1465
1442
  columns=[[name], [ValueType.PRICE]],
1466
1443
  dtype="float64",
@@ -10,9 +10,9 @@ from typing import TYPE_CHECKING, Callable, cast
10
10
  from numpy import (
11
11
  append,
12
12
  array,
13
- dot,
14
13
  float64,
15
14
  inf,
15
+ isnan,
16
16
  linspace,
17
17
  nan,
18
18
  sqrt,
@@ -105,11 +105,8 @@ def simulate_portfolios(
105
105
  weights = weights / npsum(weights)
106
106
  all_weights[x, :] = weights
107
107
 
108
- vol_arr[x] = sqrt(
109
- dot(
110
- weights.T,
111
- dot(log_ret.cov() * simframe.periods_in_a_year, weights),
112
- ),
108
+ vol_arr[x] = sqrt(weights.T @
109
+ (log_ret.cov() * simframe.periods_in_a_year @ weights),
113
110
  )
114
111
 
115
112
  ret_arr[x] = npsum(log_ret.mean() * weights * simframe.periods_in_a_year)
@@ -182,10 +179,11 @@ def efficient_frontier( # noqa: C901
182
179
  simulated = simulate_portfolios(simframe=copi, num_ports=num_ports, seed=seed)
183
180
 
184
181
  frontier_min = simulated.loc[simulated["stdev"].idxmin()]["ret"]
185
- arithmetic_mean = log_ret.mean() * copi.periods_in_a_year
186
- frontier_max = 0.0
187
- if isinstance(arithmetic_mean, Series):
188
- frontier_max = arithmetic_mean.max()
182
+
183
+ arithmetic_means = array(log_ret.mean() * copi.periods_in_a_year)
184
+ cleaned_arithmetic_means = arithmetic_means[~isnan(arithmetic_means)]
185
+
186
+ frontier_max = cleaned_arithmetic_means.max()
189
187
 
190
188
  def _check_sum(weights: NDArray[float64]) -> float64:
191
189
  return cast(float64, npsum(weights) - 1)
@@ -196,7 +194,7 @@ def efficient_frontier( # noqa: C901
196
194
  per_in_yr: float,
197
195
  ) -> NDArray[float64]:
198
196
  ret = npsum(lg_ret.mean() * weights) * per_in_yr
199
- volatility = sqrt(dot(weights.T, dot(lg_ret.cov() * per_in_yr, weights)))
197
+ volatility = sqrt(weights.T @ (lg_ret.cov() * per_in_yr @ weights))
200
198
  sr = ret / volatility
201
199
  return cast(NDArray[float64], array([ret, volatility, sr]))
202
200
 
@@ -509,6 +507,10 @@ def sharpeplot( # noqa: C901
509
507
  fig, logo = load_plotly_dict()
510
508
  figure = Figure(fig)
511
509
 
510
+ if sim_frame is None and line_frame is None and point_frame is None:
511
+ msg = "One of sim_frame, line_frame or point_frame must be provided."
512
+ raise ValueError(msg)
513
+
512
514
  if sim_frame is not None:
513
515
  returns.extend(list(sim_frame.loc[:, "ret"]))
514
516
  risk.extend(list(sim_frame.loc[:, "stdev"]))
@@ -543,11 +545,11 @@ def sharpeplot( # noqa: C901
543
545
  name="Efficient frontier",
544
546
  )
545
547
 
546
- colorway = fig["layout"].get("colorway")[ # type: ignore[union-attr]
547
- : len(cast(DataFrame, point_frame).columns)
548
- ]
549
-
550
548
  if point_frame is not None:
549
+ colorway = cast(
550
+ dict[str, str | int | float | bool | list[str]],
551
+ fig["layout"],
552
+ ).get("colorway")[: len(point_frame.columns)]
551
553
  for col, clr in zip(point_frame.columns, colorway):
552
554
  returns.extend([point_frame.loc["ret", col]])
553
555
  risk.extend([point_frame.loc["stdev", col]])
openseries/series.py CHANGED
@@ -25,11 +25,11 @@ from pandas import (
25
25
  Series,
26
26
  date_range,
27
27
  )
28
- from pydantic import model_validator
28
+ from pydantic import field_validator, model_validator
29
29
  from typing_extensions import Self
30
30
 
31
31
  from ._common_model import _CommonModel
32
- from .datefixer import date_fix, do_resample_to_business_period_ends
32
+ from .datefixer import _do_resample_to_business_period_ends, date_fix
33
33
  from .types import (
34
34
  Countries,
35
35
  CountriesType,
@@ -105,8 +105,22 @@ class OpenTimeSeries(_CommonModel):
105
105
  isin: str | None = None
106
106
  label: str | None = None
107
107
 
108
+ @field_validator("domestic", mode="before")
109
+ @classmethod
110
+ def _validate_domestic(cls, value: CurrencyStringType) -> CurrencyStringType:
111
+ """Pydantic validator to ensure domestic field is validated."""
112
+ _ = Currency(ccy=value)
113
+ return value
114
+
115
+ @field_validator("countries", mode="before")
116
+ @classmethod
117
+ def _validate_countries(cls, value: CountriesType) -> CountriesType:
118
+ """Pydantic validator to ensure countries field is validated."""
119
+ _ = Countries(countryinput=value)
120
+ return value
121
+
108
122
  @model_validator(mode="after") # type: ignore[misc,unused-ignore]
109
- def dates_and_values_validate(self: Self) -> Self:
123
+ def _dates_and_values_validate(self: Self) -> Self:
110
124
  """Pydantic validator to ensure dates and values are validated."""
111
125
  values_list_length = len(self.values)
112
126
  dates_list_length = len(self.dates)
@@ -126,28 +140,6 @@ class OpenTimeSeries(_CommonModel):
126
140
  raise ValueError(msg)
127
141
  return self
128
142
 
129
- @classmethod
130
- def setup_class(
131
- cls: type[OpenTimeSeries],
132
- domestic_ccy: CurrencyStringType = "SEK",
133
- countries: CountriesType = "SE",
134
- ) -> None:
135
- """Set the domestic currency and calendar of the user.
136
-
137
- Parameters
138
- ----------
139
- domestic_ccy : CurrencyStringType, default: "SEK"
140
- Currency code according to ISO 4217
141
- countries: CountriesType, default: "SE"
142
- (List of) country code(s) according to ISO 3166-1 alpha-2
143
-
144
- """
145
- _ = Currency(ccy=domestic_ccy)
146
- _ = Countries(countryinput=countries)
147
-
148
- cls.domestic = domestic_ccy
149
- cls.countries = countries
150
-
151
143
  @classmethod
152
144
  def from_arrays(
153
145
  cls: type[OpenTimeSeries],
@@ -241,14 +233,17 @@ class OpenTimeSeries(_CommonModel):
241
233
  An OpenTimeSeries object
242
234
 
243
235
  """
244
- if isinstance(dframe, Series):
245
- if isinstance(dframe.name, tuple):
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
+ ):
246
241
  label, _ = dframe.name
247
242
  else:
248
243
  label = dframe.name
249
244
  values = dframe.to_numpy().tolist()
250
- else:
251
- values = cast(DataFrame, dframe).iloc[:, column_nmbr].tolist()
245
+ elif isinstance(dframe, DataFrame):
246
+ values = dframe.iloc[:, column_nmbr].to_list()
252
247
  if isinstance(dframe.columns, MultiIndex):
253
248
  if _check_if_none(
254
249
  dframe.columns.get_level_values(0).to_numpy()[column_nmbr],
@@ -270,6 +265,9 @@ class OpenTimeSeries(_CommonModel):
270
265
  ]
271
266
  else:
272
267
  label = cast(MultiIndex, dframe.columns).to_numpy()[column_nmbr]
268
+ else:
269
+ raise TypeError(msg)
270
+
273
271
  dates = [date_fix(d).strftime("%Y-%m-%d") for d in dframe.index]
274
272
 
275
273
  return cls(
@@ -341,9 +339,7 @@ class OpenTimeSeries(_CommonModel):
341
339
  )
342
340
  elif not isinstance(d_range, DatetimeIndex) and not all([days, end_dt]):
343
341
  msg = "If d_range is not provided both days and end_dt must be."
344
- raise ValueError(
345
- msg,
346
- )
342
+ raise ValueError(msg)
347
343
 
348
344
  deltas = array(
349
345
  [
@@ -584,12 +580,8 @@ class OpenTimeSeries(_CommonModel):
584
580
  An OpenTimeSeries object
585
581
 
586
582
  """
587
- head = self.tsdf.iloc[0].copy()
588
- tail = self.tsdf.iloc[-1].copy()
589
- dates = do_resample_to_business_period_ends(
583
+ dates = _do_resample_to_business_period_ends(
590
584
  data=self.tsdf,
591
- head=head,
592
- tail=tail,
593
585
  freq=freq,
594
586
  countries=self.countries,
595
587
  )
openseries/simulation.py CHANGED
@@ -153,8 +153,8 @@ class ReturnSimulation(BaseModel):
153
153
  mean_annual_return: float,
154
154
  mean_annual_vol: PositiveFloat,
155
155
  trading_days: PositiveInt,
156
- seed: int,
157
156
  trading_days_in_year: DaysInYearType = 252,
157
+ seed: int | None = None,
158
158
  randomizer: Generator | None = None,
159
159
  ) -> ReturnSimulation:
160
160
  """Create a Normal distribution simulation.
@@ -169,11 +169,10 @@ class ReturnSimulation(BaseModel):
169
169
  Mean return
170
170
  mean_annual_vol: PositiveFloat
171
171
  Mean standard deviation
172
- seed: int
173
- Seed for random process initiation
174
- trading_days_in_year: DaysInYearType,
175
- default: 252
172
+ trading_days_in_year: DaysInYearType, default: 252
176
173
  Number of trading days used to annualize
174
+ seed: int, optional
175
+ Seed for random process initiation
177
176
  randomizer: numpy.random.Generator, optional
178
177
  Random process generator
179
178
 
@@ -209,8 +208,8 @@ class ReturnSimulation(BaseModel):
209
208
  mean_annual_return: float,
210
209
  mean_annual_vol: PositiveFloat,
211
210
  trading_days: PositiveInt,
212
- seed: int,
213
211
  trading_days_in_year: DaysInYearType = 252,
212
+ seed: int | None = None,
214
213
  randomizer: Generator | None = None,
215
214
  ) -> ReturnSimulation:
216
215
  """Create a Lognormal distribution simulation.
@@ -225,11 +224,10 @@ class ReturnSimulation(BaseModel):
225
224
  Mean return
226
225
  mean_annual_vol: PositiveFloat
227
226
  Mean standard deviation
228
- seed: int
229
- Seed for random process initiation
230
- trading_days_in_year: DaysInYearType,
231
- default: 252
227
+ trading_days_in_year: DaysInYearType, default: 252
232
228
  Number of trading days used to annualize
229
+ seed: int, optional
230
+ Seed for random process initiation
233
231
  randomizer: numpy.random.Generator, optional
234
232
  Random process generator
235
233
 
@@ -268,8 +266,8 @@ class ReturnSimulation(BaseModel):
268
266
  mean_annual_return: float,
269
267
  mean_annual_vol: PositiveFloat,
270
268
  trading_days: PositiveInt,
271
- seed: int,
272
269
  trading_days_in_year: DaysInYearType = 252,
270
+ seed: int | None = None,
273
271
  randomizer: Generator | None = None,
274
272
  ) -> ReturnSimulation:
275
273
  """Create a Geometric Brownian Motion simulation.
@@ -284,10 +282,10 @@ class ReturnSimulation(BaseModel):
284
282
  Mean return
285
283
  mean_annual_vol: PositiveFloat
286
284
  Mean standard deviation
287
- seed: int
288
- Seed for random process initiation
289
285
  trading_days_in_year: DaysInYearType, default: 252
290
286
  Number of trading days used to annualize
287
+ seed: int, optional
288
+ Seed for random process initiation
291
289
  randomizer: numpy.random.Generator, optional
292
290
  Random process generator
293
291
 
@@ -330,11 +328,11 @@ class ReturnSimulation(BaseModel):
330
328
  trading_days: PositiveInt,
331
329
  mean_annual_return: float,
332
330
  mean_annual_vol: PositiveFloat,
333
- seed: int,
334
331
  jumps_lamda: NonNegativeFloat,
335
332
  jumps_sigma: NonNegativeFloat = 0.0,
336
333
  jumps_mu: float = 0.0,
337
334
  trading_days_in_year: DaysInYearType = 252,
335
+ seed: int | None = None,
338
336
  randomizer: Generator | None = None,
339
337
  ) -> ReturnSimulation:
340
338
  """Create a Merton Jump-Diffusion model simulation.
@@ -349,8 +347,6 @@ class ReturnSimulation(BaseModel):
349
347
  Mean return
350
348
  mean_annual_vol: PositiveFloat
351
349
  Mean standard deviation
352
- seed: int
353
- Seed for random process initiation
354
350
  jumps_lamda: NonNegativeFloat
355
351
  This is the probability of a jump happening at each point in time
356
352
  jumps_sigma: NonNegativeFloat, default: 0.0
@@ -359,6 +355,8 @@ class ReturnSimulation(BaseModel):
359
355
  This is the average jump size
360
356
  trading_days_in_year: DaysInYearType, default: 252
361
357
  Number of trading days used to annualize
358
+ seed: int, optional
359
+ Seed for random process initiation
362
360
  randomizer: numpy.random.Generator, optional
363
361
  Random process generator
364
362
 
openseries/types.py CHANGED
@@ -272,9 +272,7 @@ class OpenTimeSeriesPropertiesList(list[str]):
272
272
  msg = (
273
273
  f"Invalid string: {item}. Allowed strings: {self.allowed_strings}"
274
274
  )
275
- raise ValueError(
276
- msg,
277
- )
275
+ raise ValueError(msg)
278
276
  if item in seen:
279
277
  msg = f"Duplicate string: {item}"
280
278
  raise ValueError(msg)
@@ -323,9 +321,7 @@ class OpenFramePropertiesList(list[str]):
323
321
  msg = (
324
322
  f"Invalid string: {item}. Allowed strings: {self.allowed_strings}"
325
323
  )
326
- raise ValueError(
327
- msg,
328
- )
324
+ raise ValueError(msg)
329
325
  if item in seen:
330
326
  msg = f"Duplicate string: {item}"
331
327
  raise ValueError(msg)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openseries
3
- Version: 1.7.0
3
+ Version: 1.7.2
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,<2.0.0)
31
+ Requires-Dist: scipy (>=1.11.4,<1.14.1)
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
@@ -261,7 +261,6 @@ make lint
261
261
 
262
262
  | Method | Applies to | Description |
263
263
  |:-------------------------|:-----------------|:-----------------------------------------------------------------------------------------------------------------------------------------------|
264
- | `setup_class` | `OpenTimeSeries` | Class method that defines the `domestic` home currency and the `countries` home countries attributes. |
265
264
  | `pandas_df` | `OpenTimeSeries` | Method to create the `tsdf` pandas.DataFrame from the `dates` and `values`. |
266
265
  | `set_new_label` | `OpenTimeSeries` | Method to change the pandas.DataFrame column MultiIndex. |
267
266
  | `running_adjustment` | `OpenTimeSeries` | Adjusts the series performance with a `float` factor. |
@@ -0,0 +1,16 @@
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,,
@@ -1,16 +0,0 @@
1
- openseries/__init__.py,sha256=k2r-yXfmxqBT7_voQ-4HL5dBxfkQIItsm-o77ot3aDQ,1116
2
- openseries/_common_model.py,sha256=smkqaErhDGbcihsqzFeF7tbwstlmlkgcPF2C1KaaGbU,74369
3
- openseries/_risk.py,sha256=PReIfkzhInvIgJkzI4k1wYvhmLZ4cCurYKuQAvlHLlE,2082
4
- openseries/datefixer.py,sha256=CFsZfooXIl2q7w8x8O2xRwfmQP67offwbWEaNYpEJGU,12778
5
- openseries/frame.py,sha256=6u5UGLSVyaBC3YdB84mF3iBSw70Gwnrk4iBQiNeJppg,55580
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=asD8n8m8roEN0SuJK_LoLc5OnTcPmFVl64598dG2sUQ,18749
10
- openseries/series.py,sha256=WqDSGOEBFuVQkfdrysXS4PBi2B7HFT-v9f41vt9JaSA,27984
11
- openseries/simulation.py,sha256=QpeEC2nsG3DAqd51spAccrYWhUbsFWfCyyCkrjCMd7Y,13860
12
- openseries/types.py,sha256=KoqDGI-DOJGP3zPzhx0an7IGIttWG0zYw-VJqolc1xA,7908
13
- openseries-1.7.0.dist-info/LICENSE.md,sha256=IQ8_IMXgHxyv4M48G14fJsjcrkiSASdalASTXWCOsj4,1515
14
- openseries-1.7.0.dist-info/METADATA,sha256=G82OxH9BK1fRJVI45Lr-PNu_hbHKmdo1uKnZZa8JIx0,44166
15
- openseries-1.7.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
16
- openseries-1.7.0.dist-info/RECORD,,