openseries 1.9.2__py3-none-any.whl → 1.9.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.
openseries/frame.py CHANGED
@@ -7,13 +7,13 @@ 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="index,assignment,arg-type,no-any-return"
10
+ # mypy: disable-error-code="assignment,no-any-return"
11
11
  from __future__ import annotations
12
12
 
13
13
  from copy import deepcopy
14
14
  from functools import reduce
15
15
  from logging import getLogger
16
- from typing import TYPE_CHECKING, cast
16
+ from typing import TYPE_CHECKING, Any, cast
17
17
 
18
18
  if TYPE_CHECKING: # pragma: no cover
19
19
  import datetime as dt
@@ -26,7 +26,6 @@ from numpy import (
26
26
  log,
27
27
  nan,
28
28
  sqrt,
29
- square,
30
29
  std,
31
30
  )
32
31
  from pandas import (
@@ -58,6 +57,7 @@ from .owntypes import (
58
57
  NoWeightsError,
59
58
  OpenFramePropertiesList,
60
59
  RatioInputError,
60
+ ResampleDataLossError,
61
61
  Self,
62
62
  ValueType,
63
63
  )
@@ -127,12 +127,14 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
127
127
  Object of the class OpenFrame
128
128
 
129
129
  """
130
+ copied_constituents = [ts.from_deepcopy() for ts in constituents]
131
+
130
132
  super().__init__( # type: ignore[call-arg]
131
- constituents=constituents,
133
+ constituents=copied_constituents,
132
134
  weights=weights,
133
135
  )
134
136
 
135
- self.constituents = constituents
137
+ self.constituents = copied_constituents
136
138
  self.weights = weights
137
139
  self._set_tsdf()
138
140
 
@@ -341,8 +343,11 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
341
343
  """
342
344
  returns = self.tsdf.ffill().pct_change()
343
345
  returns.iloc[0] = 0
344
- new_labels = [ValueType.RTRN] * self.item_count
345
- arrays = [self.tsdf.columns.get_level_values(0), new_labels]
346
+ new_labels: list[ValueType] = [ValueType.RTRN] * self.item_count
347
+ arrays: list[Index[Any], list[ValueType]] = [ # type: ignore[type-arg]
348
+ self.tsdf.columns.get_level_values(0),
349
+ new_labels,
350
+ ]
346
351
  returns.columns = MultiIndex.from_arrays(arrays=arrays)
347
352
  self.tsdf = returns.copy()
348
353
  return self
@@ -364,8 +369,11 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
364
369
  """
365
370
  self.tsdf = self.tsdf.diff(periods=periods)
366
371
  self.tsdf.iloc[0] = 0
367
- new_labels = [ValueType.RTRN] * self.item_count
368
- arrays = [self.tsdf.columns.get_level_values(0), new_labels]
372
+ new_labels: list[ValueType] = [ValueType.RTRN] * self.item_count
373
+ arrays: list[Index[Any], list[ValueType]] = [ # type: ignore[type-arg]
374
+ self.tsdf.columns.get_level_values(0),
375
+ new_labels,
376
+ ]
369
377
  self.tsdf.columns = MultiIndex.from_arrays(arrays)
370
378
  return self
371
379
 
@@ -392,8 +400,11 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
392
400
  returns = returns.add(1.0)
393
401
  self.tsdf = returns.cumprod(axis=0) / returns.iloc[0]
394
402
 
395
- new_labels = [ValueType.PRICE] * self.item_count
396
- arrays = [self.tsdf.columns.get_level_values(0), new_labels]
403
+ new_labels: list[ValueType] = [ValueType.PRICE] * self.item_count
404
+ arrays: list[Index[Any], list[ValueType]] = [ # type: ignore[type-arg]
405
+ self.tsdf.columns.get_level_values(0),
406
+ new_labels,
407
+ ]
397
408
  self.tsdf.columns = MultiIndex.from_arrays(arrays)
398
409
  return self
399
410
 
@@ -414,12 +425,27 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
414
425
  An OpenFrame object
415
426
 
416
427
  """
428
+ vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
429
+ if not any(vtypes):
430
+ value_type = ValueType.PRICE
431
+ elif all(vtypes):
432
+ value_type = ValueType.RTRN
433
+ else:
434
+ msg = "Mix of series types will give inconsistent results"
435
+ raise MixedValuetypesError(msg)
436
+
417
437
  self.tsdf.index = DatetimeIndex(self.tsdf.index)
418
- self.tsdf = self.tsdf.resample(freq).last()
438
+ if value_type == ValueType.PRICE:
439
+ self.tsdf = self.tsdf.resample(freq).last()
440
+ else:
441
+ self.tsdf = self.tsdf.resample(freq).sum()
419
442
  self.tsdf.index = Index(d.date() for d in DatetimeIndex(self.tsdf.index))
420
443
  for xerie in self.constituents:
421
444
  xerie.tsdf.index = DatetimeIndex(xerie.tsdf.index)
422
- xerie.tsdf = xerie.tsdf.resample(freq).last()
445
+ if value_type == ValueType.PRICE:
446
+ xerie.tsdf = xerie.tsdf.resample(freq).last()
447
+ else:
448
+ xerie.tsdf = xerie.tsdf.resample(freq).sum()
423
449
  xerie.tsdf.index = Index(
424
450
  dejt.date() for dejt in DatetimeIndex(xerie.tsdf.index)
425
451
  )
@@ -448,6 +474,15 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
448
474
  An OpenFrame object
449
475
 
450
476
  """
477
+ vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
478
+ if any(vtypes):
479
+ msg = (
480
+ "Do not run resample_to_business_period_ends on return series. "
481
+ "The operation will pick the last data point in the sparser series. "
482
+ "It will not sum returns and therefore data will be lost."
483
+ )
484
+ raise ResampleDataLossError(msg)
485
+
451
486
  for xerie in self.constituents:
452
487
  dates = _do_resample_to_business_period_ends(
453
488
  data=xerie.tsdf,
@@ -478,6 +513,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
478
513
  dlta_degr_freedms: int = 0,
479
514
  first_column: int = 0,
480
515
  second_column: int = 1,
516
+ corr_scale: float = 2.0,
481
517
  months_from_last: int | None = None,
482
518
  from_date: dt.date | None = None,
483
519
  to_date: dt.date | None = None,
@@ -500,6 +536,8 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
500
536
  Column of first timeseries.
501
537
  second_column: int, default: 1
502
538
  Column of second timeseries.
539
+ corr_scale: float, default: 2.0
540
+ Correlation scale factor.
503
541
  months_from_last : int, optional
504
542
  number of months offset as positive integer. Overrides use of from_date
505
543
  and to_date
@@ -517,7 +555,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
517
555
  Series volatilities and correlation
518
556
 
519
557
  """
520
- earlier, later = self.calc_range(months_from_last, from_date, to_date)
558
+ earlier, later = self.calc_range(
559
+ months_offset=months_from_last, from_dt=from_date, to_dt=to_date
560
+ )
521
561
  if periods_in_a_year_fixed is None:
522
562
  fraction = (later - earlier).days / 365.25
523
563
  how_many = (
@@ -542,9 +582,7 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
542
582
  data = self.tsdf.loc[cast("int", earlier) : cast("int", later)].copy()
543
583
 
544
584
  for rtn in cols:
545
- data[rtn, ValueType.RTRN] = (
546
- data.loc[:, (rtn, ValueType.PRICE)].apply(log).diff()
547
- )
585
+ data[rtn, ValueType.RTRN] = log(data.loc[:, (rtn, ValueType.PRICE)]).diff()
548
586
 
549
587
  raw_one = [
550
588
  data.loc[:, (cols[0], ValueType.RTRN)]
@@ -565,34 +603,39 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
565
603
  ddof=dlta_degr_freedms,
566
604
  )[0][1],
567
605
  ]
568
- raw_corr = [raw_cov[0] / (2 * raw_one[0] * raw_two[0])]
569
606
 
570
- for _, row in data.iloc[1:].iterrows():
571
- tmp_raw_one = sqrt(
572
- square(row.loc[cols[0], ValueType.RTRN]) * time_factor * (1 - lmbda)
573
- + square(raw_one[-1]) * lmbda,
574
- )
575
- tmp_raw_two = sqrt(
576
- square(row.loc[cols[1], ValueType.RTRN]) * time_factor * (1 - lmbda)
577
- + square(raw_two[-1]) * lmbda,
578
- )
579
- tmp_raw_cov = (
580
- row.loc[cols[0], ValueType.RTRN]
581
- * row.loc[cols[1], ValueType.RTRN]
582
- * time_factor
583
- * (1 - lmbda)
584
- + raw_cov[-1] * lmbda
585
- )
586
- tmp_raw_corr = tmp_raw_cov / (2 * tmp_raw_one * tmp_raw_two)
587
- raw_one.append(tmp_raw_one)
588
- raw_two.append(tmp_raw_two)
589
- raw_cov.append(tmp_raw_cov)
590
- raw_corr.append(tmp_raw_corr)
607
+ r1 = data.loc[:, (cols[0], ValueType.RTRN)]
608
+ r2 = data.loc[:, (cols[1], ValueType.RTRN)]
609
+
610
+ alpha = 1.0 - lmbda
611
+
612
+ s1 = (r1.pow(2) * time_factor).copy()
613
+ s2 = (r2.pow(2) * time_factor).copy()
614
+ sc = (r1 * r2 * time_factor).copy()
615
+
616
+ s1.iloc[0] = float(raw_one[0] ** 2)
617
+ s2.iloc[0] = float(raw_two[0] ** 2)
618
+ sc.iloc[0] = float(raw_cov[0])
619
+
620
+ m1 = s1.ewm(alpha=alpha, adjust=False).mean()
621
+ m2 = s2.ewm(alpha=alpha, adjust=False).mean()
622
+ mc = sc.ewm(alpha=alpha, adjust=False).mean()
623
+
624
+ m1v = m1.to_numpy(copy=False)
625
+ m2v = m2.to_numpy(copy=False)
626
+ mcv = mc.to_numpy(copy=False)
627
+
628
+ vol1 = sqrt(m1v)
629
+ vol2 = sqrt(m2v)
630
+ denom = corr_scale * vol1 * vol2
631
+
632
+ corr = mcv / denom
633
+ corr[denom == 0.0] = nan
591
634
 
592
635
  return DataFrame(
593
636
  index=[*cols, corr_label],
594
637
  columns=data.index,
595
- data=[raw_one, raw_two, raw_corr],
638
+ data=[vol1, vol2, corr],
596
639
  ).T
597
640
 
598
641
  @property
@@ -793,7 +836,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
793
836
  Tracking Errors
794
837
 
795
838
  """
796
- earlier, later = self.calc_range(months_from_last, from_date, to_date)
839
+ earlier, later = self.calc_range(
840
+ months_offset=months_from_last, from_dt=from_date, to_dt=to_date
841
+ )
797
842
  fraction = (later - earlier).days / 365.25
798
843
 
799
844
  msg = "base_column should be a tuple[str, ValueType] or an integer."
@@ -836,10 +881,8 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
836
881
  :,
837
882
  item,
838
883
  ]
839
- relative = 1.0 + longdf - shortdf
840
- vol = float(
841
- relative.ffill().pct_change().std() * sqrt(time_factor),
842
- )
884
+ relative = longdf.ffill().pct_change() - shortdf.ffill().pct_change()
885
+ vol = float(relative.std() * sqrt(time_factor))
843
886
  terrors.append(vol)
844
887
 
845
888
  return Series(
@@ -885,7 +928,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
885
928
  Information Ratios
886
929
 
887
930
  """
888
- earlier, later = self.calc_range(months_from_last, from_date, to_date)
931
+ earlier, later = self.calc_range(
932
+ months_offset=months_from_last, from_dt=from_date, to_dt=to_date
933
+ )
889
934
  fraction = (later - earlier).days / 365.25
890
935
 
891
936
  msg = "base_column should be a tuple[str, ValueType] or an integer."
@@ -928,13 +973,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
928
973
  :,
929
974
  item,
930
975
  ]
931
- relative = 1.0 + longdf - shortdf
932
- ret = float(
933
- relative.ffill().pct_change().mean() * time_factor,
934
- )
935
- vol = float(
936
- relative.ffill().pct_change().std() * sqrt(time_factor),
937
- )
976
+ relative = longdf.ffill().pct_change() - shortdf.ffill().pct_change()
977
+ ret = float(relative.mean() * time_factor)
978
+ vol = float(relative.std() * sqrt(time_factor))
938
979
  ratios.append(ret / vol)
939
980
 
940
981
  return Series(
@@ -988,7 +1029,9 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
988
1029
 
989
1030
  """
990
1031
  loss_limit: float = 0.0
991
- earlier, later = self.calc_range(months_from_last, from_date, to_date)
1032
+ earlier, later = self.calc_range(
1033
+ months_offset=months_from_last, from_dt=from_date, to_dt=to_date
1034
+ )
992
1035
  fraction = (later - earlier).days / 365.25
993
1036
 
994
1037
  msg = "base_column should be a tuple[str, ValueType] or an integer."
@@ -1169,10 +1212,8 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1169
1212
  Beta as Co-variance of x & y divided by Variance of x
1170
1213
 
1171
1214
  """
1172
- if all(
1173
- x_value == ValueType.RTRN
1174
- for x_value in self.tsdf.columns.get_level_values(1).to_numpy()
1175
- ):
1215
+ vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
1216
+ if all(vtypes):
1176
1217
  msg = "asset should be a tuple[str, ValueType] or an integer."
1177
1218
  if isinstance(asset, tuple):
1178
1219
  y_value = self.tsdf.loc[:, asset]
@@ -1188,33 +1229,27 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1188
1229
  x_value = self.tsdf.iloc[:, market]
1189
1230
  else:
1190
1231
  raise TypeError(msg)
1191
- else:
1232
+ elif not any(vtypes):
1192
1233
  msg = "asset should be a tuple[str, ValueType] or an integer."
1193
1234
  if isinstance(asset, tuple):
1194
- y_value = log(
1195
- self.tsdf.loc[:, asset] / self.tsdf.loc[:, asset].iloc[0],
1196
- )
1235
+ y_value = self.tsdf.loc[:, asset].ffill().pct_change().iloc[1:]
1197
1236
  elif isinstance(asset, int):
1198
- y_value = log(
1199
- self.tsdf.iloc[:, asset] / cast("float", self.tsdf.iloc[0, asset]),
1200
- )
1237
+ y_value = self.tsdf.iloc[:, asset].ffill().pct_change().iloc[1:]
1201
1238
  else:
1202
1239
  raise TypeError(msg)
1203
-
1204
1240
  msg = "market should be a tuple[str, ValueType] or an integer."
1241
+
1205
1242
  if isinstance(market, tuple):
1206
- x_value = log(
1207
- self.tsdf.loc[:, market] / self.tsdf.loc[:, market].iloc[0],
1208
- )
1243
+ x_value = self.tsdf.loc[:, market].ffill().pct_change().iloc[1:]
1209
1244
  elif isinstance(market, int):
1210
- x_value = log(
1211
- self.tsdf.iloc[:, market]
1212
- / cast("float", self.tsdf.iloc[0, market]),
1213
- )
1245
+ x_value = self.tsdf.iloc[:, market].ffill().pct_change().iloc[1:]
1214
1246
  else:
1215
1247
  raise TypeError(msg)
1248
+ else:
1249
+ msg = "Mix of series types will give inconsistent results"
1250
+ raise MixedValuetypesError(msg)
1216
1251
 
1217
- covariance = cov(y_value, x_value, ddof=dlta_degr_freedms)
1252
+ covariance = cov(m=y_value, y=x_value, ddof=dlta_degr_freedms)
1218
1253
  beta = covariance[0, 1] / covariance[1, 1]
1219
1254
 
1220
1255
  return float(beta)
@@ -1315,105 +1350,57 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1315
1350
  Jensen's alpha
1316
1351
 
1317
1352
  """
1318
- full_year = 1.0
1319
1353
  vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
1320
1354
  if not any(vtypes):
1321
1355
  msg = "asset should be a tuple[str, ValueType] or an integer."
1322
1356
  if isinstance(asset, tuple):
1323
- asset_log = log(
1324
- self.tsdf.loc[:, asset] / self.tsdf.loc[:, asset].iloc[0],
1325
- )
1326
- if self.yearfrac > full_year:
1327
- asset_cagr = (
1328
- self.tsdf.loc[:, asset].iloc[-1]
1329
- / self.tsdf.loc[:, asset].iloc[0]
1330
- ) ** (1 / self.yearfrac) - 1
1331
- else:
1332
- asset_cagr = (
1333
- self.tsdf.loc[:, asset].iloc[-1]
1334
- / self.tsdf.loc[:, asset].iloc[0]
1335
- - 1
1336
- )
1357
+ asset_rtn = self.tsdf.loc[:, asset].ffill().pct_change().iloc[1:]
1358
+ asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
1337
1359
  elif isinstance(asset, int):
1338
- asset_log = log(
1339
- self.tsdf.iloc[:, asset] / cast("float", self.tsdf.iloc[0, asset]),
1340
- )
1341
- if self.yearfrac > full_year:
1342
- asset_cagr = (
1343
- cast("float", self.tsdf.iloc[-1, asset])
1344
- / cast("float", self.tsdf.iloc[0, asset])
1345
- ) ** (1 / self.yearfrac) - 1
1346
- else:
1347
- asset_cagr = (
1348
- cast("float", self.tsdf.iloc[-1, asset])
1349
- / cast("float", self.tsdf.iloc[0, asset])
1350
- - 1
1351
- )
1360
+ asset_rtn = self.tsdf.iloc[:, asset].ffill().pct_change().iloc[1:]
1361
+ asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
1352
1362
  else:
1353
1363
  raise TypeError(msg)
1354
1364
 
1355
1365
  msg = "market should be a tuple[str, ValueType] or an integer."
1356
1366
  if isinstance(market, tuple):
1357
- market_log = log(
1358
- self.tsdf.loc[:, market] / self.tsdf.loc[:, market].iloc[0],
1359
- )
1360
- if self.yearfrac > full_year:
1361
- market_cagr = (
1362
- self.tsdf.loc[:, market].iloc[-1]
1363
- / self.tsdf.loc[:, market].iloc[0]
1364
- ) ** (1 / self.yearfrac) - 1
1365
- else:
1366
- market_cagr = (
1367
- self.tsdf.loc[:, market].iloc[-1]
1368
- / self.tsdf.loc[:, market].iloc[0]
1369
- - 1
1370
- )
1367
+ market_rtn = self.tsdf.loc[:, market].ffill().pct_change().iloc[1:]
1368
+ market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
1371
1369
  elif isinstance(market, int):
1372
- market_log = log(
1373
- self.tsdf.iloc[:, market]
1374
- / cast("float", self.tsdf.iloc[0, market]),
1375
- )
1376
- if self.yearfrac > full_year:
1377
- market_cagr = (
1378
- cast("float", self.tsdf.iloc[-1, market])
1379
- / cast("float", self.tsdf.iloc[0, market])
1380
- ) ** (1 / self.yearfrac) - 1
1381
- else:
1382
- market_cagr = (
1383
- cast("float", self.tsdf.iloc[-1, market])
1384
- / cast("float", self.tsdf.iloc[0, market])
1385
- - 1
1386
- )
1370
+ market_rtn = self.tsdf.iloc[:, market].ffill().pct_change().iloc[1:]
1371
+ market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
1387
1372
  else:
1388
1373
  raise TypeError(msg)
1389
1374
  elif all(vtypes):
1390
1375
  msg = "asset should be a tuple[str, ValueType] or an integer."
1391
1376
  if isinstance(asset, tuple):
1392
- asset_log = self.tsdf.loc[:, asset]
1393
- asset_cagr = asset_log.mean()
1377
+ asset_rtn = self.tsdf.loc[:, asset]
1378
+ asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
1394
1379
  elif isinstance(asset, int):
1395
- asset_log = self.tsdf.iloc[:, asset]
1396
- asset_cagr = asset_log.mean()
1380
+ asset_rtn = self.tsdf.iloc[:, asset]
1381
+ asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
1397
1382
  else:
1398
1383
  raise TypeError(msg)
1399
1384
 
1400
1385
  msg = "market should be a tuple[str, ValueType] or an integer."
1401
1386
  if isinstance(market, tuple):
1402
- market_log = self.tsdf.loc[:, market]
1403
- market_cagr = market_log.mean()
1387
+ market_rtn = self.tsdf.loc[:, market]
1388
+ market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
1404
1389
  elif isinstance(market, int):
1405
- market_log = self.tsdf.iloc[:, market]
1406
- market_cagr = market_log.mean()
1390
+ market_rtn = self.tsdf.iloc[:, market]
1391
+ market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
1407
1392
  else:
1408
1393
  raise TypeError(msg)
1409
1394
  else:
1410
1395
  msg = "Mix of series types will give inconsistent results"
1411
1396
  raise MixedValuetypesError(msg)
1412
1397
 
1413
- covariance = cov(asset_log, market_log, ddof=dlta_degr_freedms)
1398
+ covariance = cov(m=asset_rtn, y=market_rtn, ddof=dlta_degr_freedms)
1414
1399
  beta = covariance[0, 1] / covariance[1, 1]
1415
1400
 
1416
- return float(asset_cagr - riskfree_rate - beta * (market_cagr - riskfree_rate))
1401
+ return float(
1402
+ asset_rtn_mean - riskfree_rate - beta * (market_rtn_mean - riskfree_rate)
1403
+ )
1417
1404
 
1418
1405
  def make_portfolio(
1419
1406
  self: Self,
@@ -1647,3 +1634,62 @@ class OpenFrame(_CommonModel): # type: ignore[misc]
1647
1634
  )
1648
1635
 
1649
1636
  return DataFrame(corrdf)
1637
+
1638
+ def multi_factor_linear_regression(
1639
+ self: Self,
1640
+ dependent_column: tuple[str, ValueType],
1641
+ ) -> tuple[DataFrame, OpenTimeSeries]:
1642
+ """Perform a multi-factor linear regression.
1643
+
1644
+ This function treats one specified column in the DataFrame as the dependent
1645
+ variable (y) and uses all remaining columns as independent variables (X).
1646
+ It utilizes a scikit-learn LinearRegression model and returns a DataFrame
1647
+ with summary output and an OpenTimeSeries of predicted values.
1648
+
1649
+ Parameters
1650
+ ----------
1651
+ dependent_column: tuple[str, ValueType]
1652
+ A tuple key to select the column in the OpenFrame.tsdf.columns
1653
+ to use as the dependent variable
1654
+
1655
+ Returns:
1656
+ -------
1657
+ tuple[pandas.DataFrame, OpenTimeSeries]
1658
+ - A DataFrame with the R-squared, the intercept
1659
+ and the regression coefficients
1660
+ - An OpenTimeSeries of predicted values
1661
+
1662
+ Raises:
1663
+ KeyError: If the column tuple is not found in the OpenFrame.tsdf.columns
1664
+ ValueError: If not all series are returnseries (ValueType.RTRN)
1665
+ """
1666
+ key_msg = (
1667
+ f"Tuple ({dependent_column[0]}, "
1668
+ f"{dependent_column[1].value}) not found in data."
1669
+ )
1670
+ if dependent_column not in self.tsdf.columns:
1671
+ raise KeyError(key_msg)
1672
+
1673
+ vtype_msg = "All series should be of ValueType.RTRN."
1674
+ if not all(x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)):
1675
+ raise MixedValuetypesError(vtype_msg)
1676
+
1677
+ dependent = self.tsdf[dependent_column]
1678
+ factors = self.tsdf.drop(columns=[dependent_column])
1679
+ indx = ["R-square", "Intercept", *factors.columns.droplevel(level=1)]
1680
+
1681
+ model = LinearRegression()
1682
+ model.fit(factors, dependent)
1683
+
1684
+ predictions = OpenTimeSeries.from_arrays(
1685
+ name=f"Predicted {dependent_column[0]}",
1686
+ dates=[date.strftime("%Y-%m-%d") for date in self.tsdf.index],
1687
+ values=list(model.predict(factors)),
1688
+ valuetype=ValueType.RTRN,
1689
+ )
1690
+
1691
+ output = [model.score(factors, dependent), model.intercept_, *model.coef_]
1692
+
1693
+ result = DataFrame(data=output, index=indx, columns=[dependent_column[0]])
1694
+
1695
+ return result, predictions.to_cumret()
openseries/load_plotly.py CHANGED
@@ -76,7 +76,7 @@ def load_plotly_dict(
76
76
  with logofile.open(mode="r", encoding="utf-8") as logo_file:
77
77
  logo = load(logo_file)
78
78
 
79
- if _check_remote_file_existence(url=logo["source"]) is False:
79
+ if not _check_remote_file_existence(url=logo["source"]):
80
80
  msg = f"Failed to add logo image from URL {logo['source']}"
81
81
  logger.warning(msg)
82
82
  logo = {}
openseries/owntypes.py CHANGED
@@ -167,6 +167,7 @@ LiteralSeriesProps = Literal[
167
167
  "downside_deviation",
168
168
  "ret_vol_ratio",
169
169
  "sortino_ratio",
170
+ "kappa3_ratio",
170
171
  "z_score",
171
172
  "skew",
172
173
  "kurtosis",
@@ -194,6 +195,7 @@ LiteralFrameProps = Literal[
194
195
  "downside_deviation",
195
196
  "ret_vol_ratio",
196
197
  "sortino_ratio",
198
+ "kappa3_ratio",
197
199
  "z_score",
198
200
  "skew",
199
201
  "kurtosis",
@@ -224,6 +226,7 @@ class PropertiesList(list[str]):
224
226
  "downside_deviation",
225
227
  "ret_vol_ratio",
226
228
  "sortino_ratio",
229
+ "kappa3_ratio",
227
230
  "omega_ratio",
228
231
  "z_score",
229
232
  "skew",
@@ -373,3 +376,7 @@ class IncorrectArgumentComboError(Exception):
373
376
 
374
377
  class PropertiesInputValidationError(Exception):
375
378
  """Raised when duplicate strings are provided."""
379
+
380
+
381
+ class ResampleDataLossError(Exception):
382
+ """Raised when user attempts to run resample_to_business_period_ends on returns."""
@@ -7,7 +7,7 @@ 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="index,assignment"
10
+ # mypy: disable-error-code="assignment"
11
11
  from __future__ import annotations
12
12
 
13
13
  from inspect import stack
@@ -378,9 +378,11 @@ def constrain_optimized_portfolios(
378
378
  condition_least_ret = front_frame.ret > serie.arithmetic_ret
379
379
  # noinspection PyArgumentList
380
380
  least_ret_frame = front_frame[condition_least_ret].sort_values(by="stdev")
381
- least_ret_port = least_ret_frame.iloc[0]
381
+ least_ret_port: Series[float] = least_ret_frame.iloc[0]
382
382
  least_ret_port_name = f"Minimize vol & target return of {portfolioname}"
383
- least_ret_weights = [least_ret_port[c] for c in lr_frame.columns_lvl_zero]
383
+ least_ret_weights: list[float] = [
384
+ least_ret_port.loc[c] for c in lr_frame.columns_lvl_zero
385
+ ]
384
386
  lr_frame.weights = least_ret_weights
385
387
  resleast = OpenTimeSeries.from_df(lr_frame.make_portfolio(least_ret_port_name))
386
388
 
@@ -390,9 +392,11 @@ def constrain_optimized_portfolios(
390
392
  by="ret",
391
393
  ascending=False,
392
394
  )
393
- most_vol_port = most_vol_frame.iloc[0]
395
+ most_vol_port: Series[float] = most_vol_frame.iloc[0]
394
396
  most_vol_port_name = f"Maximize return & target risk of {portfolioname}"
395
- most_vol_weights = [most_vol_port[c] for c in mv_frame.columns_lvl_zero]
397
+ most_vol_weights: list[float] = [
398
+ most_vol_port.loc[c] for c in mv_frame.columns_lvl_zero
399
+ ]
396
400
  mv_frame.weights = most_vol_weights
397
401
  resmost = OpenTimeSeries.from_df(mv_frame.make_portfolio(most_vol_port_name))
398
402
 
@@ -562,7 +566,7 @@ def sharpeplot(
562
566
  )
563
567
 
564
568
  if point_frame is not None:
565
- colorway = cast(
569
+ colorway = cast( # type: ignore[index]
566
570
  "dict[str, str | int | float | bool | list[str]]",
567
571
  fig["layout"],
568
572
  ).get("colorway")[: len(point_frame.columns)]
openseries/report.py CHANGED
@@ -7,7 +7,7 @@ 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,index,arg-type"
10
+ # mypy: disable-error-code="assignment"
11
11
  from __future__ import annotations
12
12
 
13
13
  from inspect import stack
@@ -68,9 +68,7 @@ def calendar_period_returns(
68
68
  """
69
69
  copied = data.from_deepcopy()
70
70
  copied.resample_to_business_period_ends(freq=freq)
71
- vtypes = [x == ValueType.RTRN for x in copied.tsdf.columns.get_level_values(1)]
72
- if not any(vtypes):
73
- copied.value_to_ret()
71
+ copied.value_to_ret()
74
72
  cldr = copied.tsdf.iloc[1:].copy()
75
73
  if relabel:
76
74
  if freq.upper() == "BYE":
@@ -244,7 +242,7 @@ def report_html(
244
242
  x=bdf.index,
245
243
  y=bdf.iloc[:, item],
246
244
  hovertemplate="%{y:.2%}<br>%{x}",
247
- name=bdf.iloc[:, item].name[0],
245
+ name=bdf.iloc[:, item].name[0], # type: ignore[index]
248
246
  showlegend=False,
249
247
  row=2,
250
248
  col=1,
@@ -265,7 +263,7 @@ def report_html(
265
263
  ]
266
264
 
267
265
  # noinspection PyTypeChecker
268
- rpt_df = data.all_properties(properties=properties)
266
+ rpt_df = data.all_properties(properties=properties) # type: ignore[arg-type]
269
267
  alpha_frame = data.from_deepcopy()
270
268
  alpha_frame.to_cumret()
271
269
  with catch_warnings():