openseries 1.7.3__tar.gz → 1.7.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openseries
3
- Version: 1.7.3
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
@@ -59,6 +59,7 @@ from .types import (
59
59
  )
60
60
 
61
61
 
62
+ # noinspection PyTypeChecker
62
63
  class _CommonModel(BaseModel):
63
64
  """Declare _CommonModel."""
64
65
 
@@ -680,7 +681,7 @@ class _CommonModel(BaseModel):
680
681
  output.append(dict(itemdata))
681
682
 
682
683
  with dirpath.joinpath(filename).open(mode="w", encoding="utf-8") as jsonfile:
683
- dump(output, jsonfile, indent=2, sort_keys=False)
684
+ dump(obj=output, fp=jsonfile, indent=2, sort_keys=False)
684
685
 
685
686
  return output
686
687
 
@@ -1027,7 +1028,10 @@ class _CommonModel(BaseModel):
1027
1028
  time_factor = how_many / fraction
1028
1029
 
1029
1030
  result = (
1030
- self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change().mean()
1031
+ self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1032
+ .ffill()
1033
+ .pct_change()
1034
+ .mean()
1031
1035
  * time_factor
1032
1036
  )
1033
1037
 
@@ -1085,7 +1089,7 @@ class _CommonModel(BaseModel):
1085
1089
  time_factor = how_many / fraction
1086
1090
 
1087
1091
  data = self.tsdf.loc[cast(int, earlier) : cast(int, later)]
1088
- result = data.pct_change().std().mul(sqrt(time_factor))
1092
+ result = data.ffill().pct_change().std().mul(sqrt(time_factor))
1089
1093
 
1090
1094
  if self.tsdf.shape[1] == 1:
1091
1095
  return float(cast(SupportsFloat, result.iloc[0]))
@@ -14,7 +14,6 @@ if TYPE_CHECKING:
14
14
  import statsmodels.api as sm # type: ignore[import-untyped,unused-ignore]
15
15
  from numpy import (
16
16
  cov,
17
- cumprod,
18
17
  divide,
19
18
  isinf,
20
19
  log,
@@ -336,11 +335,12 @@ class OpenFrame(_CommonModel):
336
335
  The returns of the values in the series
337
336
 
338
337
  """
339
- self.tsdf = self.tsdf.pct_change()
340
- self.tsdf.iloc[0] = 0
338
+ returns = self.tsdf.ffill().pct_change()
339
+ returns.iloc[0] = 0
341
340
  new_labels = [ValueType.RTRN] * self.item_count
342
341
  arrays = [self.tsdf.columns.get_level_values(0), new_labels]
343
- self.tsdf.columns = MultiIndex.from_arrays(arrays)
342
+ returns.columns = MultiIndex.from_arrays(arrays=arrays)
343
+ self.tsdf = returns.copy()
344
344
  return self
345
345
 
346
346
  def value_to_diff(self: Self, periods: int = 1) -> Self:
@@ -374,14 +374,20 @@ class OpenFrame(_CommonModel):
374
374
  An OpenFrame object
375
375
 
376
376
  """
377
- if any(
378
- x == ValueType.PRICE
379
- for x in self.tsdf.columns.get_level_values(1).to_numpy()
380
- ):
381
- self.value_to_ret()
377
+ vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
378
+ if not any(vtypes):
379
+ returns = self.tsdf.ffill().pct_change()
380
+ returns.iloc[0] = 0
381
+ elif all(vtypes):
382
+ returns = self.tsdf.copy()
383
+ returns.iloc[0] = 0
384
+ else:
385
+ msg = "Mix of series types will give inconsistent results"
386
+ raise ValueError(msg)
387
+
388
+ returns = returns.add(1.0)
389
+ self.tsdf = returns.cumprod(axis=0) / returns.iloc[0]
382
390
 
383
- self.tsdf = self.tsdf.add(1.0)
384
- self.tsdf = self.tsdf.apply(cumprod, axis="index") / self.tsdf.iloc[0]
385
391
  new_labels = [ValueType.PRICE] * self.item_count
386
392
  arrays = [self.tsdf.columns.get_level_values(0), new_labels]
387
393
  self.tsdf.columns = MultiIndex.from_arrays(arrays)
@@ -453,8 +459,15 @@ class OpenFrame(_CommonModel):
453
459
  method=method,
454
460
  )
455
461
 
462
+ arrays = [
463
+ self.tsdf.columns.get_level_values(0),
464
+ self.tsdf.columns.get_level_values(1),
465
+ ]
466
+
456
467
  self._set_tsdf()
457
468
 
469
+ self.tsdf.columns = MultiIndex.from_arrays(arrays)
470
+
458
471
  return self
459
472
 
460
473
  def ewma_risk(
@@ -1284,30 +1297,8 @@ class OpenFrame(_CommonModel):
1284
1297
 
1285
1298
  """
1286
1299
  full_year = 1.0
1287
- if all(
1288
- x == ValueType.RTRN
1289
- for x in self.tsdf.columns.get_level_values(1).to_numpy()
1290
- ):
1291
- msg = "asset should be a tuple[str, ValueType] or an integer."
1292
- if isinstance(asset, tuple):
1293
- asset_log = self.tsdf.loc[:, asset]
1294
- asset_cagr = asset_log.mean()
1295
- elif isinstance(asset, int):
1296
- asset_log = self.tsdf.iloc[:, asset]
1297
- asset_cagr = asset_log.mean()
1298
- else:
1299
- raise TypeError(msg)
1300
-
1301
- msg = "market should be a tuple[str, ValueType] or an integer."
1302
- if isinstance(market, tuple):
1303
- market_log = self.tsdf.loc[:, market]
1304
- market_cagr = market_log.mean()
1305
- elif isinstance(market, int):
1306
- market_log = self.tsdf.iloc[:, market]
1307
- market_cagr = market_log.mean()
1308
- else:
1309
- raise TypeError(msg)
1310
- else:
1300
+ vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
1301
+ if not any(vtypes):
1311
1302
  msg = "asset should be a tuple[str, ValueType] or an integer."
1312
1303
  if isinstance(asset, tuple):
1313
1304
  asset_log = log(
@@ -1375,6 +1366,29 @@ class OpenFrame(_CommonModel):
1375
1366
  )
1376
1367
  else:
1377
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)
1378
1392
 
1379
1393
  covariance = cov(asset_log, market_log, ddof=dlta_degr_freedms)
1380
1394
  beta = covariance[0, 1] / covariance[1, 1]
@@ -1407,27 +1421,30 @@ class OpenFrame(_CommonModel):
1407
1421
  "to run the make_portfolio method."
1408
1422
  )
1409
1423
  raise ValueError(msg)
1410
- dframe = self.tsdf.copy()
1411
- if not any(
1412
- x == ValueType.RTRN
1413
- for x in self.tsdf.columns.get_level_values(1).to_numpy()
1414
- ):
1415
- dframe = dframe.pct_change()
1416
- dframe.iloc[0] = 0
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)
1417
1434
 
1418
1435
  msg = "Weight strategy not implemented"
1419
1436
  if weight_strat:
1420
1437
  if weight_strat == "eq_weights":
1421
1438
  self.weights = [1.0 / self.item_count] * self.item_count
1422
1439
  elif weight_strat == "inv_vol":
1423
- vol = divide(1.0, std(dframe, axis=0, ddof=1))
1440
+ vol = divide(1.0, std(returns, axis=0, ddof=1))
1424
1441
  vol[isinf(vol)] = nan
1425
1442
  self.weights = list(divide(vol, vol.sum()))
1426
1443
  else:
1427
1444
  raise NotImplementedError(msg)
1428
1445
 
1429
1446
  return DataFrame(
1430
- data=(dframe @ self.weights).add(1.0).cumprod(),
1447
+ data=(returns @ self.weights).add(1.0).cumprod(),
1431
1448
  index=self.tsdf.index,
1432
1449
  columns=[[name], [ValueType.PRICE]],
1433
1450
  dtype="float64",
@@ -83,13 +83,15 @@ def simulate_portfolios(
83
83
  """
84
84
  copi = simframe.from_deepcopy()
85
85
 
86
- if any(
87
- x == ValueType.PRICE for x in copi.tsdf.columns.get_level_values(1).to_numpy()
88
- ):
86
+ vtypes = [x == ValueType.RTRN for x in copi.tsdf.columns.get_level_values(1)]
87
+ if not any(vtypes):
89
88
  copi.value_to_ret()
90
89
  log_ret = copi.tsdf.copy()[1:]
91
- else:
90
+ elif all(vtypes):
92
91
  log_ret = copi.tsdf.copy()
92
+ else:
93
+ msg = "Mix of series types will give inconsistent results"
94
+ raise ValueError(msg)
93
95
 
94
96
  log_ret.columns = log_ret.columns.droplevel(level=1)
95
97
 
@@ -165,13 +167,15 @@ def efficient_frontier( # noqa: C901
165
167
 
166
168
  copi = eframe.from_deepcopy()
167
169
 
168
- if any(
169
- x == ValueType.PRICE for x in copi.tsdf.columns.get_level_values(1).to_numpy()
170
- ):
170
+ vtypes = [x == ValueType.RTRN for x in copi.tsdf.columns.get_level_values(1)]
171
+ if not any(vtypes):
171
172
  copi.value_to_ret()
172
173
  log_ret = copi.tsdf.copy()[1:]
173
- else:
174
+ elif all(vtypes):
174
175
  log_ret = copi.tsdf.copy()
176
+ else:
177
+ msg = "Mix of series types will give inconsistent results"
178
+ raise ValueError(msg)
175
179
 
176
180
  log_ret.columns = log_ret.columns.droplevel(level=1)
177
181
 
@@ -346,6 +346,7 @@ class OpenTimeSeries(_CommonModel):
346
346
  - cast(DatetimeIndex, d_range)[:-1]
347
347
  ],
348
348
  )
349
+ # noinspection PyTypeChecker
349
350
  arr = list(cumprod(insert(1 + deltas * rate / 365, 0, 1.0)))
350
351
  dates = [d.strftime("%Y-%m-%d") for d in cast(DatetimeIndex, d_range)]
351
352
 
@@ -434,15 +435,12 @@ class OpenTimeSeries(_CommonModel):
434
435
  The returns of the values in the series
435
436
 
436
437
  """
437
- self.tsdf = self.tsdf.pct_change()
438
- self.tsdf.iloc[0] = 0
438
+ returns = self.tsdf.ffill().pct_change()
439
+ returns.iloc[0] = 0
439
440
  self.valuetype = ValueType.RTRN
440
- self.tsdf.columns = MultiIndex.from_arrays(
441
- [
442
- [self.label],
443
- [self.valuetype],
444
- ],
445
- )
441
+ arrays = [[self.label], [self.valuetype]]
442
+ returns.columns = MultiIndex.from_arrays(arrays=arrays)
443
+ self.tsdf = returns.copy()
446
444
  return self
447
445
 
448
446
  def value_to_diff(self: Self, periods: int = 1) -> Self:
@@ -480,14 +478,12 @@ class OpenTimeSeries(_CommonModel):
480
478
  An OpenTimeSeries object
481
479
 
482
480
  """
483
- if not any(
484
- x == ValueType.RTRN
485
- for x in cast(MultiIndex, self.tsdf.columns).get_level_values(1).to_numpy()
486
- ):
481
+ if self.valuetype == ValueType.PRICE:
487
482
  self.value_to_ret()
488
483
 
489
484
  self.tsdf = self.tsdf.add(1.0)
490
485
  self.tsdf = self.tsdf.cumprod(axis=0) / self.tsdf.iloc[0]
486
+
491
487
  self.valuetype = ValueType.PRICE
492
488
  self.tsdf.columns = MultiIndex.from_arrays(
493
489
  [
@@ -520,6 +516,7 @@ class OpenTimeSeries(_CommonModel):
520
516
  arr = array(self.values) / divider
521
517
 
522
518
  deltas = array([i.days for i in self.tsdf.index[1:] - self.tsdf.index[:-1]])
519
+ # noinspection PyTypeChecker
523
520
  arr = cumprod(insert(1.0 + deltas * arr[:-1] / days_in_year, 0, 1.0))
524
521
 
525
522
  self.dates = [d.strftime("%Y-%m-%d") for d in self.tsdf.index]
@@ -684,11 +681,7 @@ class OpenTimeSeries(_CommonModel):
684
681
  An OpenTimeSeries object
685
682
 
686
683
  """
687
- values: list[float]
688
- if any(
689
- x == ValueType.RTRN
690
- for x in cast(MultiIndex, self.tsdf.columns).get_level_values(1).to_numpy()
691
- ):
684
+ if self.valuetype == ValueType.RTRN:
692
685
  ra_df = self.tsdf.copy()
693
686
  values = [1.0]
694
687
  returns_input = True
@@ -818,6 +811,7 @@ def timeseries_chain(
818
811
 
819
812
  dates.extend([x.strftime("%Y-%m-%d") for x in new.tsdf.index])
820
813
 
814
+ # noinspection PyUnresolvedReferences
821
815
  if back.__class__.__subclasscheck__(
822
816
  OpenTimeSeries,
823
817
  ):
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "openseries"
3
- version = "1.7.3"
3
+ version = "1.7.4"
4
4
  description = "Tools for analyzing financial timeseries."
5
5
  authors = ["Martin Karrin <martin.karrin@captor.se>"]
6
6
  repository = "https://github.com/CaptorAB/openseries"
@@ -51,9 +51,9 @@ coverage = ">=7.6.0,<8.0.0"
51
51
  genbadge = {version = ">=1.1.1,<2.0.0", extras = ["coverage"]}
52
52
  mypy = "^1.11.2"
53
53
  pandas-stubs = ">=2.1.2,<3.0.0"
54
- pre-commit = ">=3.7.1,<4.0.0"
54
+ pre-commit = ">=3.7.1,<6.0.0"
55
55
  pytest = ">=8.2.2,<9.0.0"
56
- ruff = "^0.6.5"
56
+ ruff = "^0.6.9"
57
57
  types-openpyxl = ">=3.1.2,<4.0.0"
58
58
  types-python-dateutil = ">=2.8.2,<3.0.0"
59
59
  types-requests = ">=2.20.0,<3.0.0"
@@ -110,7 +110,7 @@ fixable = ["ALL"]
110
110
 
111
111
  [tool.ruff.lint.pylint]
112
112
  max-args = 12
113
- max-branches = 22
113
+ max-branches = 23
114
114
  max-statements = 59
115
115
 
116
116
  [tool.pytest.ini_options]
File without changes
File without changes