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/_common_model.py +96 -28
- openseries/frame.py +188 -142
- openseries/load_plotly.py +1 -1
- openseries/owntypes.py +7 -0
- openseries/portfoliotools.py +10 -6
- openseries/report.py +4 -6
- openseries/series.py +21 -7
- openseries/simulation.py +2 -3
- {openseries-1.9.2.dist-info → openseries-1.9.4.dist-info}/METADATA +64 -62
- openseries-1.9.4.dist-info/RECORD +17 -0
- openseries-1.9.2.dist-info/RECORD +0 -17
- {openseries-1.9.2.dist-info → openseries-1.9.4.dist-info}/LICENSE.md +0 -0
- {openseries-1.9.2.dist-info → openseries-1.9.4.dist-info}/WHEEL +0 -0
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="
|
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=
|
133
|
+
constituents=copied_constituents,
|
132
134
|
weights=weights,
|
133
135
|
)
|
134
136
|
|
135
|
-
self.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 = [
|
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 = [
|
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 = [
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
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=[
|
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(
|
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 =
|
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(
|
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 =
|
932
|
-
ret = float(
|
933
|
-
|
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(
|
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
|
-
|
1173
|
-
|
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
|
-
|
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 =
|
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 =
|
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 =
|
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 =
|
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
|
-
|
1324
|
-
|
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
|
-
|
1339
|
-
|
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
|
-
|
1358
|
-
|
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
|
-
|
1373
|
-
|
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
|
-
|
1393
|
-
|
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
|
-
|
1396
|
-
|
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
|
-
|
1403
|
-
|
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
|
-
|
1406
|
-
|
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(
|
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(
|
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"])
|
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."""
|
openseries/portfoliotools.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="
|
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
|
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
|
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
|
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
|
-
|
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():
|