openseries 1.9.1__py3-none-any.whl → 1.9.3__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,22 +7,17 @@ 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
20
20
 
21
- from statsmodels.regression.linear_model import ( # type: ignore[import-untyped]
22
- OLSResults,
23
- )
24
-
25
- import statsmodels.api as sm # type: ignore[import-untyped]
26
21
  from numpy import (
27
22
  array,
28
23
  cov,
@@ -31,7 +26,6 @@ from numpy import (
31
26
  log,
32
27
  nan,
33
28
  sqrt,
34
- square,
35
29
  std,
36
30
  )
37
31
  from pandas import (
@@ -44,6 +38,7 @@ from pandas import (
44
38
  merge,
45
39
  )
46
40
  from pydantic import field_validator
41
+ from sklearn.linear_model import LinearRegression
47
42
 
48
43
  from ._common_model import _CommonModel
49
44
  from .datefixer import _do_resample_to_business_period_ends
@@ -54,8 +49,6 @@ from .owntypes import (
54
49
  LiteralCaptureRatio,
55
50
  LiteralFrameProps,
56
51
  LiteralHowMerge,
57
- LiteralOlsFitCovType,
58
- LiteralOlsFitMethod,
59
52
  LiteralPandasReindexMethod,
60
53
  LiteralPortfolioWeightings,
61
54
  LiteralTrunc,
@@ -75,7 +68,7 @@ __all__ = ["OpenFrame"]
75
68
 
76
69
 
77
70
  # noinspection PyUnresolvedReferences,PyTypeChecker
78
- class OpenFrame(_CommonModel):
71
+ class OpenFrame(_CommonModel): # type: ignore[misc]
79
72
  """OpenFrame objects hold OpenTimeSeries in the list constituents.
80
73
 
81
74
  The intended use is to allow comparisons across these timeseries.
@@ -133,12 +126,14 @@ class OpenFrame(_CommonModel):
133
126
  Object of the class OpenFrame
134
127
 
135
128
  """
129
+ copied_constituents = [ts.from_deepcopy() for ts in constituents]
130
+
136
131
  super().__init__( # type: ignore[call-arg]
137
- constituents=constituents,
132
+ constituents=copied_constituents,
138
133
  weights=weights,
139
134
  )
140
135
 
141
- self.constituents = constituents
136
+ self.constituents = copied_constituents
142
137
  self.weights = weights
143
138
  self._set_tsdf()
144
139
 
@@ -345,10 +340,13 @@ class OpenFrame(_CommonModel):
345
340
  The returns of the values in the series
346
341
 
347
342
  """
348
- returns = self.tsdf.ffill().pct_change()
343
+ returns = self.tsdf.pct_change()
349
344
  returns.iloc[0] = 0
350
- new_labels = [ValueType.RTRN] * self.item_count
351
- arrays = [self.tsdf.columns.get_level_values(0), new_labels]
345
+ new_labels: list[ValueType] = [ValueType.RTRN] * self.item_count
346
+ arrays: list[Index[Any], list[ValueType]] = [ # type: ignore[type-arg]
347
+ self.tsdf.columns.get_level_values(0),
348
+ new_labels,
349
+ ]
352
350
  returns.columns = MultiIndex.from_arrays(arrays=arrays)
353
351
  self.tsdf = returns.copy()
354
352
  return self
@@ -370,8 +368,11 @@ class OpenFrame(_CommonModel):
370
368
  """
371
369
  self.tsdf = self.tsdf.diff(periods=periods)
372
370
  self.tsdf.iloc[0] = 0
373
- new_labels = [ValueType.RTRN] * self.item_count
374
- arrays = [self.tsdf.columns.get_level_values(0), new_labels]
371
+ new_labels: list[ValueType] = [ValueType.RTRN] * self.item_count
372
+ arrays: list[Index[Any], list[ValueType]] = [ # type: ignore[type-arg]
373
+ self.tsdf.columns.get_level_values(0),
374
+ new_labels,
375
+ ]
375
376
  self.tsdf.columns = MultiIndex.from_arrays(arrays)
376
377
  return self
377
378
 
@@ -386,7 +387,7 @@ class OpenFrame(_CommonModel):
386
387
  """
387
388
  vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
388
389
  if not any(vtypes):
389
- returns = self.tsdf.ffill().pct_change()
390
+ returns = self.tsdf.pct_change()
390
391
  returns.iloc[0] = 0
391
392
  elif all(vtypes):
392
393
  returns = self.tsdf.copy()
@@ -398,8 +399,11 @@ class OpenFrame(_CommonModel):
398
399
  returns = returns.add(1.0)
399
400
  self.tsdf = returns.cumprod(axis=0) / returns.iloc[0]
400
401
 
401
- new_labels = [ValueType.PRICE] * self.item_count
402
- arrays = [self.tsdf.columns.get_level_values(0), new_labels]
402
+ new_labels: list[ValueType] = [ValueType.PRICE] * self.item_count
403
+ arrays: list[Index[Any], list[ValueType]] = [ # type: ignore[type-arg]
404
+ self.tsdf.columns.get_level_values(0),
405
+ new_labels,
406
+ ]
403
407
  self.tsdf.columns = MultiIndex.from_arrays(arrays)
404
408
  return self
405
409
 
@@ -484,6 +488,7 @@ class OpenFrame(_CommonModel):
484
488
  dlta_degr_freedms: int = 0,
485
489
  first_column: int = 0,
486
490
  second_column: int = 1,
491
+ corr_scale: float = 2.0,
487
492
  months_from_last: int | None = None,
488
493
  from_date: dt.date | None = None,
489
494
  to_date: dt.date | None = None,
@@ -506,6 +511,8 @@ class OpenFrame(_CommonModel):
506
511
  Column of first timeseries.
507
512
  second_column: int, default: 1
508
513
  Column of second timeseries.
514
+ corr_scale: float, default: 2.0
515
+ Correlation scale factor.
509
516
  months_from_last : int, optional
510
517
  number of months offset as positive integer. Overrides use of from_date
511
518
  and to_date
@@ -548,9 +555,7 @@ class OpenFrame(_CommonModel):
548
555
  data = self.tsdf.loc[cast("int", earlier) : cast("int", later)].copy()
549
556
 
550
557
  for rtn in cols:
551
- data[rtn, ValueType.RTRN] = (
552
- data.loc[:, (rtn, ValueType.PRICE)].apply(log).diff()
553
- )
558
+ data[rtn, ValueType.RTRN] = log(data.loc[:, (rtn, ValueType.PRICE)]).diff()
554
559
 
555
560
  raw_one = [
556
561
  data.loc[:, (cols[0], ValueType.RTRN)]
@@ -571,34 +576,39 @@ class OpenFrame(_CommonModel):
571
576
  ddof=dlta_degr_freedms,
572
577
  )[0][1],
573
578
  ]
574
- raw_corr = [raw_cov[0] / (2 * raw_one[0] * raw_two[0])]
575
579
 
576
- for _, row in data.iloc[1:].iterrows():
577
- tmp_raw_one = sqrt(
578
- square(row.loc[cols[0], ValueType.RTRN]) * time_factor * (1 - lmbda)
579
- + square(raw_one[-1]) * lmbda,
580
- )
581
- tmp_raw_two = sqrt(
582
- square(row.loc[cols[1], ValueType.RTRN]) * time_factor * (1 - lmbda)
583
- + square(raw_two[-1]) * lmbda,
584
- )
585
- tmp_raw_cov = (
586
- row.loc[cols[0], ValueType.RTRN]
587
- * row.loc[cols[1], ValueType.RTRN]
588
- * time_factor
589
- * (1 - lmbda)
590
- + raw_cov[-1] * lmbda
591
- )
592
- tmp_raw_corr = tmp_raw_cov / (2 * tmp_raw_one * tmp_raw_two)
593
- raw_one.append(tmp_raw_one)
594
- raw_two.append(tmp_raw_two)
595
- raw_cov.append(tmp_raw_cov)
596
- raw_corr.append(tmp_raw_corr)
580
+ r1 = data.loc[:, (cols[0], ValueType.RTRN)]
581
+ r2 = data.loc[:, (cols[1], ValueType.RTRN)]
582
+
583
+ alpha = 1.0 - lmbda
584
+
585
+ s1 = (r1.pow(2) * time_factor).copy()
586
+ s2 = (r2.pow(2) * time_factor).copy()
587
+ sc = (r1 * r2 * time_factor).copy()
588
+
589
+ s1.iloc[0] = float(raw_one[0] ** 2)
590
+ s2.iloc[0] = float(raw_two[0] ** 2)
591
+ sc.iloc[0] = float(raw_cov[0])
592
+
593
+ m1 = s1.ewm(alpha=alpha, adjust=False).mean()
594
+ m2 = s2.ewm(alpha=alpha, adjust=False).mean()
595
+ mc = sc.ewm(alpha=alpha, adjust=False).mean()
596
+
597
+ m1v = m1.to_numpy(copy=False)
598
+ m2v = m2.to_numpy(copy=False)
599
+ mcv = mc.to_numpy(copy=False)
600
+
601
+ vol1 = sqrt(m1v)
602
+ vol2 = sqrt(m2v)
603
+ denom = corr_scale * vol1 * vol2
604
+
605
+ corr = mcv / denom
606
+ corr[denom == 0.0] = nan
597
607
 
598
608
  return DataFrame(
599
609
  index=[*cols, corr_label],
600
610
  columns=data.index,
601
- data=[raw_one, raw_two, raw_corr],
611
+ data=[vol1, vol2, corr],
602
612
  ).T
603
613
 
604
614
  @property
@@ -611,13 +621,9 @@ class OpenFrame(_CommonModel):
611
621
  Correlation matrix
612
622
 
613
623
  """
614
- corr_matrix = (
615
- self.tsdf.ffill()
616
- .pct_change()
617
- .corr(
618
- method="pearson",
619
- min_periods=1,
620
- )
624
+ corr_matrix = self.tsdf.pct_change().corr(
625
+ method="pearson",
626
+ min_periods=1,
621
627
  )
622
628
  corr_matrix.columns = corr_matrix.columns.droplevel(level=1)
623
629
  corr_matrix.index = corr_matrix.index.droplevel(level=1)
@@ -844,7 +850,7 @@ class OpenFrame(_CommonModel):
844
850
  ]
845
851
  relative = 1.0 + longdf - shortdf
846
852
  vol = float(
847
- relative.ffill().pct_change().std() * sqrt(time_factor),
853
+ relative.pct_change().std() * sqrt(time_factor),
848
854
  )
849
855
  terrors.append(vol)
850
856
 
@@ -936,10 +942,10 @@ class OpenFrame(_CommonModel):
936
942
  ]
937
943
  relative = 1.0 + longdf - shortdf
938
944
  ret = float(
939
- relative.ffill().pct_change().mean() * time_factor,
945
+ relative.pct_change().mean() * time_factor,
940
946
  )
941
947
  vol = float(
942
- relative.ffill().pct_change().std() * sqrt(time_factor),
948
+ relative.pct_change().std() * sqrt(time_factor),
943
949
  )
944
950
  ratios.append(ret / vol)
945
951
 
@@ -1040,18 +1046,16 @@ class OpenFrame(_CommonModel):
1040
1046
  msg = "ratio must be one of 'up', 'down' or 'both'."
1041
1047
  if ratio == "up":
1042
1048
  uparray = (
1043
- longdf.ffill()
1044
- .pct_change()[
1045
- shortdf.ffill().pct_change().to_numpy() > loss_limit
1049
+ longdf.pct_change()[
1050
+ shortdf.pct_change().to_numpy() > loss_limit
1046
1051
  ]
1047
1052
  .add(1)
1048
1053
  .to_numpy()
1049
1054
  )
1050
1055
  up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
1051
1056
  upidxarray = (
1052
- shortdf.ffill()
1053
- .pct_change()[
1054
- shortdf.ffill().pct_change().to_numpy() > loss_limit
1057
+ shortdf.pct_change()[
1058
+ shortdf.pct_change().to_numpy() > loss_limit
1055
1059
  ]
1056
1060
  .add(1)
1057
1061
  .to_numpy()
@@ -1062,9 +1066,8 @@ class OpenFrame(_CommonModel):
1062
1066
  ratios.append(up_rtrn / up_idx_return)
1063
1067
  elif ratio == "down":
1064
1068
  downarray = (
1065
- longdf.ffill()
1066
- .pct_change()[
1067
- shortdf.ffill().pct_change().to_numpy() < loss_limit
1069
+ longdf.pct_change()[
1070
+ shortdf.pct_change().to_numpy() < loss_limit
1068
1071
  ]
1069
1072
  .add(1)
1070
1073
  .to_numpy()
@@ -1073,9 +1076,8 @@ class OpenFrame(_CommonModel):
1073
1076
  downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
1074
1077
  )
1075
1078
  downidxarray = (
1076
- shortdf.ffill()
1077
- .pct_change()[
1078
- shortdf.ffill().pct_change().to_numpy() < loss_limit
1079
+ shortdf.pct_change()[
1080
+ shortdf.pct_change().to_numpy() < loss_limit
1079
1081
  ]
1080
1082
  .add(1)
1081
1083
  .to_numpy()
@@ -1087,18 +1089,16 @@ class OpenFrame(_CommonModel):
1087
1089
  ratios.append(down_return / down_idx_return)
1088
1090
  elif ratio == "both":
1089
1091
  uparray = (
1090
- longdf.ffill()
1091
- .pct_change()[
1092
- shortdf.ffill().pct_change().to_numpy() > loss_limit
1092
+ longdf.pct_change()[
1093
+ shortdf.pct_change().to_numpy() > loss_limit
1093
1094
  ]
1094
1095
  .add(1)
1095
1096
  .to_numpy()
1096
1097
  )
1097
1098
  up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
1098
1099
  upidxarray = (
1099
- shortdf.ffill()
1100
- .pct_change()[
1101
- shortdf.ffill().pct_change().to_numpy() > loss_limit
1100
+ shortdf.pct_change()[
1101
+ shortdf.pct_change().to_numpy() > loss_limit
1102
1102
  ]
1103
1103
  .add(1)
1104
1104
  .to_numpy()
@@ -1107,9 +1107,8 @@ class OpenFrame(_CommonModel):
1107
1107
  upidxarray.prod() ** (1 / (len(upidxarray) / time_factor)) - 1
1108
1108
  )
1109
1109
  downarray = (
1110
- longdf.ffill()
1111
- .pct_change()[
1112
- shortdf.ffill().pct_change().to_numpy() < loss_limit
1110
+ longdf.pct_change()[
1111
+ shortdf.pct_change().to_numpy() < loss_limit
1113
1112
  ]
1114
1113
  .add(1)
1115
1114
  .to_numpy()
@@ -1118,9 +1117,8 @@ class OpenFrame(_CommonModel):
1118
1117
  downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
1119
1118
  )
1120
1119
  downidxarray = (
1121
- shortdf.ffill()
1122
- .pct_change()[
1123
- shortdf.ffill().pct_change().to_numpy() < loss_limit
1120
+ shortdf.pct_change()[
1121
+ shortdf.pct_change().to_numpy() < loss_limit
1124
1122
  ]
1125
1123
  .add(1)
1126
1124
  .to_numpy()
@@ -1229,16 +1227,13 @@ class OpenFrame(_CommonModel):
1229
1227
  self: Self,
1230
1228
  y_column: tuple[str, ValueType] | int,
1231
1229
  x_column: tuple[str, ValueType] | int,
1232
- method: LiteralOlsFitMethod = "pinv",
1233
- cov_type: LiteralOlsFitCovType = "nonrobust",
1234
1230
  *,
1235
1231
  fitted_series: bool = True,
1236
- ) -> OLSResults:
1232
+ ) -> dict[str, float]:
1237
1233
  """Ordinary Least Squares fit.
1238
1234
 
1239
1235
  Performs a linear regression and adds a new column with a fitted line
1240
1236
  using Ordinary Least Squares fit
1241
- https://www.statsmodels.org/stable/examples/notebooks/generated/ols.html.
1242
1237
 
1243
1238
  Parameters
1244
1239
  ----------
@@ -1246,50 +1241,50 @@ class OpenFrame(_CommonModel):
1246
1241
  The column level values of the dependent variable y
1247
1242
  x_column: tuple[str, ValueType] | int
1248
1243
  The column level values of the exogenous variable x
1249
- method: LiteralOlsFitMethod, default: pinv
1250
- Method to solve least squares problem
1251
- cov_type: LiteralOlsFitCovType, default: nonrobust
1252
- Covariance estimator
1253
1244
  fitted_series: bool, default: True
1254
1245
  If True the fit is added as a new column in the .tsdf Pandas.DataFrame
1255
1246
 
1256
1247
  Returns:
1257
1248
  -------
1258
- OLSResults
1259
- The Statsmodels regression output
1249
+ dict[str, float]
1250
+ A dictionary with the coefficient, intercept and rsquared outputs.
1260
1251
 
1261
1252
  """
1262
1253
  msg = "y_column should be a tuple[str, ValueType] or an integer."
1263
1254
  if isinstance(y_column, tuple):
1264
- y_value = self.tsdf.loc[:, y_column]
1255
+ y_value = self.tsdf.loc[:, y_column].to_numpy()
1265
1256
  y_label = cast(
1266
1257
  "tuple[str, str]",
1267
1258
  self.tsdf.loc[:, y_column].name,
1268
1259
  )[0]
1269
1260
  elif isinstance(y_column, int):
1270
- y_value = self.tsdf.iloc[:, y_column]
1261
+ y_value = self.tsdf.iloc[:, y_column].to_numpy()
1271
1262
  y_label = cast("tuple[str, str]", self.tsdf.iloc[:, y_column].name)[0]
1272
1263
  else:
1273
1264
  raise TypeError(msg)
1274
1265
 
1275
1266
  msg = "x_column should be a tuple[str, ValueType] or an integer."
1276
1267
  if isinstance(x_column, tuple):
1277
- x_value = self.tsdf.loc[:, x_column]
1268
+ x_value = self.tsdf.loc[:, x_column].to_numpy().reshape(-1, 1)
1278
1269
  x_label = cast(
1279
1270
  "tuple[str, str]",
1280
1271
  self.tsdf.loc[:, x_column].name,
1281
1272
  )[0]
1282
1273
  elif isinstance(x_column, int):
1283
- x_value = self.tsdf.iloc[:, x_column]
1274
+ x_value = self.tsdf.iloc[:, x_column].to_numpy().reshape(-1, 1)
1284
1275
  x_label = cast("tuple[str, str]", self.tsdf.iloc[:, x_column].name)[0]
1285
1276
  else:
1286
1277
  raise TypeError(msg)
1287
1278
 
1288
- results = sm.OLS(y_value, x_value).fit(method=method, cov_type=cov_type)
1279
+ model = LinearRegression(fit_intercept=True)
1280
+ model.fit(x_value, y_value)
1289
1281
  if fitted_series:
1290
- self.tsdf[y_label, x_label] = results.predict(x_value)
1291
-
1292
- return cast("OLSResults", results)
1282
+ self.tsdf[y_label, x_label] = model.predict(x_value)
1283
+ return {
1284
+ "coefficient": model.coef_[0],
1285
+ "intercept": model.intercept_,
1286
+ "rsquared": model.score(x_value, y_value),
1287
+ }
1293
1288
 
1294
1289
  def jensen_alpha(
1295
1290
  self: Self,
@@ -1419,7 +1414,7 @@ class OpenFrame(_CommonModel):
1419
1414
  msg = "Mix of series types will give inconsistent results"
1420
1415
  raise MixedValuetypesError(msg)
1421
1416
 
1422
- covariance = cov(asset_log, market_log, ddof=dlta_degr_freedms)
1417
+ covariance = cov(m=asset_log, y=market_log, ddof=dlta_degr_freedms)
1423
1418
  beta = covariance[0, 1] / covariance[1, 1]
1424
1419
 
1425
1420
  return float(asset_cagr - riskfree_rate - beta * (market_cagr - riskfree_rate))
@@ -1453,7 +1448,7 @@ class OpenFrame(_CommonModel):
1453
1448
 
1454
1449
  vtypes = [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
1455
1450
  if not any(vtypes):
1456
- returns = self.tsdf.ffill().pct_change()
1451
+ returns = self.tsdf.pct_change()
1457
1452
  returns.iloc[0] = 0
1458
1453
  elif all(vtypes):
1459
1454
  returns = self.tsdf.copy()
@@ -1528,14 +1523,11 @@ class OpenFrame(_CommonModel):
1528
1523
  )
1529
1524
 
1530
1525
  retseries = (
1531
- relative.ffill()
1532
- .pct_change()
1533
- .rolling(observations, min_periods=observations)
1534
- .sum()
1526
+ relative.pct_change().rolling(observations, min_periods=observations).sum()
1535
1527
  )
1536
1528
  retdf = retseries.dropna().to_frame()
1537
1529
 
1538
- voldf = relative.ffill().pct_change().rolling(
1530
+ voldf = relative.pct_change().rolling(
1539
1531
  observations,
1540
1532
  min_periods=observations,
1541
1533
  ).std() * sqrt(time_factor)
@@ -1581,13 +1573,9 @@ class OpenFrame(_CommonModel):
1581
1573
  asset_label = cast("tuple[str, str]", self.tsdf.iloc[:, asset_column].name)[0]
1582
1574
  beta_label = f"{asset_label} / {market_label}"
1583
1575
 
1584
- rolling = (
1585
- self.tsdf.ffill()
1586
- .pct_change()
1587
- .rolling(
1588
- observations,
1589
- min_periods=observations,
1590
- )
1576
+ rolling = self.tsdf.pct_change().rolling(
1577
+ observations,
1578
+ min_periods=observations,
1591
1579
  )
1592
1580
 
1593
1581
  rcov = rolling.cov(ddof=dlta_degr_freedms)
@@ -1642,11 +1630,10 @@ class OpenFrame(_CommonModel):
1642
1630
  )
1643
1631
  first_series = (
1644
1632
  self.tsdf.iloc[:, first_column]
1645
- .ffill()
1646
1633
  .pct_change()[1:]
1647
1634
  .rolling(observations, min_periods=observations)
1648
1635
  )
1649
- second_series = self.tsdf.iloc[:, second_column].ffill().pct_change()[1:]
1636
+ second_series = self.tsdf.iloc[:, second_column].pct_change()[1:]
1650
1637
  corrdf = first_series.corr(other=second_series).dropna().to_frame()
1651
1638
  corrdf.columns = MultiIndex.from_arrays(
1652
1639
  [
@@ -1656,3 +1643,62 @@ class OpenFrame(_CommonModel):
1656
1643
  )
1657
1644
 
1658
1645
  return DataFrame(corrdf)
1646
+
1647
+ def multi_factor_linear_regression(
1648
+ self: Self,
1649
+ dependent_column: tuple[str, ValueType],
1650
+ ) -> tuple[DataFrame, OpenTimeSeries]:
1651
+ """Perform a multi-factor linear regression.
1652
+
1653
+ This function treats one specified column in the DataFrame as the dependent
1654
+ variable (y) and uses all remaining columns as independent variables (X).
1655
+ It utilizes a scikit-learn LinearRegression model and returns a DataFrame
1656
+ with summary output and an OpenTimeSeries of predicted values.
1657
+
1658
+ Parameters
1659
+ ----------
1660
+ dependent_column: tuple[str, ValueType]
1661
+ A tuple key to select the column in the OpenFrame.tsdf.columns
1662
+ to use as the dependent variable
1663
+
1664
+ Returns:
1665
+ -------
1666
+ tuple[pandas.DataFrame, OpenTimeSeries]
1667
+ - A DataFrame with the R-squared, the intercept
1668
+ and the regression coefficients
1669
+ - An OpenTimeSeries of predicted values
1670
+
1671
+ Raises:
1672
+ KeyError: If the column tuple is not found in the OpenFrame.tsdf.columns
1673
+ ValueError: If not all series are returnseries (ValueType.RTRN)
1674
+ """
1675
+ key_msg = (
1676
+ f"Tuple ({dependent_column[0]}, "
1677
+ f"{dependent_column[1].value}) not found in data."
1678
+ )
1679
+ if dependent_column not in self.tsdf.columns:
1680
+ raise KeyError(key_msg)
1681
+
1682
+ vtype_msg = "All series should be of ValueType.RTRN."
1683
+ if not all(x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)):
1684
+ raise MixedValuetypesError(vtype_msg)
1685
+
1686
+ dependent = self.tsdf[dependent_column]
1687
+ factors = self.tsdf.drop(columns=[dependent_column])
1688
+ indx = ["R-square", "Intercept", *factors.columns.droplevel(level=1)]
1689
+
1690
+ model = LinearRegression()
1691
+ model.fit(factors, dependent)
1692
+
1693
+ predictions = OpenTimeSeries.from_arrays(
1694
+ name=f"Predicted {dependent_column[0]}",
1695
+ dates=[date.strftime("%Y-%m-%d") for date in self.tsdf.index],
1696
+ values=list(model.predict(factors)),
1697
+ valuetype=ValueType.RTRN,
1698
+ )
1699
+
1700
+ output = [model.score(factors, dependent), model.intercept_, *model.coef_]
1701
+
1702
+ result = DataFrame(data=output, index=indx, columns=[dependent_column[0]])
1703
+
1704
+ 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
@@ -141,21 +141,7 @@ LiteralPlotlyHistogramHistNorm = Literal[
141
141
  "density",
142
142
  "probability density",
143
143
  ]
144
- LiteralOlsFitMethod = Literal["pinv", "qr"]
145
144
  LiteralPortfolioWeightings = Literal["eq_weights", "inv_vol"]
146
- LiteralOlsFitCovType = Literal[
147
- "nonrobust",
148
- "fixed scale",
149
- "HC0",
150
- "HC1",
151
- "HC2",
152
- "HC3",
153
- "HAC",
154
- "hac-panel",
155
- "hac-groupsum",
156
- "cluster",
157
- ]
158
-
159
145
  LiteralMinimizeMethods = Literal[
160
146
  "SLSQP",
161
147
  "Nelder-Mead",
@@ -181,6 +167,7 @@ LiteralSeriesProps = Literal[
181
167
  "downside_deviation",
182
168
  "ret_vol_ratio",
183
169
  "sortino_ratio",
170
+ "kappa3_ratio",
184
171
  "z_score",
185
172
  "skew",
186
173
  "kurtosis",
@@ -208,6 +195,7 @@ LiteralFrameProps = Literal[
208
195
  "downside_deviation",
209
196
  "ret_vol_ratio",
210
197
  "sortino_ratio",
198
+ "kappa3_ratio",
211
199
  "z_score",
212
200
  "skew",
213
201
  "kurtosis",
@@ -238,6 +226,7 @@ class PropertiesList(list[str]):
238
226
  "downside_deviation",
239
227
  "ret_vol_ratio",
240
228
  "sortino_ratio",
229
+ "kappa3_ratio",
241
230
  "omega_ratio",
242
231
  "z_score",
243
232
  "skew",
@@ -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
@@ -320,7 +320,7 @@ def efficient_frontier(
320
320
 
321
321
  if tweak:
322
322
  limit_tweak = 0.001
323
- line_df["stdev_diff"] = line_df.stdev.ffill().pct_change()
323
+ line_df["stdev_diff"] = line_df.stdev.pct_change()
324
324
  line_df = line_df.loc[line_df.stdev_diff.abs() > limit_tweak]
325
325
  line_df = line_df.drop(columns="stdev_diff")
326
326
 
@@ -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)]