openseries 1.2.2__py3-none-any.whl → 1.2.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/__init__.py +1 -0
- openseries/common_model.py +265 -171
- openseries/datefixer.py +94 -111
- openseries/frame.py +252 -160
- openseries/load_plotly.py +11 -21
- openseries/risk.py +45 -23
- openseries/series.py +135 -110
- openseries/simulation.py +157 -109
- openseries/types.py +88 -21
- {openseries-1.2.2.dist-info → openseries-1.2.4.dist-info}/METADATA +11 -12
- openseries-1.2.4.dist-info/RECORD +15 -0
- {openseries-1.2.2.dist-info → openseries-1.2.4.dist-info}/WHEEL +1 -1
- openseries-1.2.2.dist-info/RECORD +0 -15
- {openseries-1.2.2.dist-info → openseries-1.2.4.dist-info}/LICENSE.md +0 -0
openseries/frame.py
CHANGED
@@ -1,62 +1,64 @@
|
|
1
|
-
"""
|
2
|
-
Defining the OpenFrame class
|
3
|
-
"""
|
1
|
+
"""Defining the OpenFrame class."""
|
4
2
|
from __future__ import annotations
|
5
|
-
|
3
|
+
|
6
4
|
import datetime as dt
|
5
|
+
from copy import deepcopy
|
7
6
|
from functools import reduce
|
8
7
|
from logging import warning
|
9
|
-
from typing import
|
10
|
-
|
8
|
+
from typing import Optional, TypeVar, Union, cast
|
9
|
+
|
10
|
+
import statsmodels.api as sm
|
11
|
+
from ffn.core import calc_erc_weights, calc_inv_vol_weights, calc_mean_var_weights
|
11
12
|
from numpy import cov, cumprod, log, sqrt
|
12
13
|
from pandas import (
|
13
|
-
concat,
|
14
14
|
DataFrame,
|
15
15
|
DatetimeIndex,
|
16
16
|
Int64Dtype,
|
17
|
-
merge,
|
18
17
|
MultiIndex,
|
19
18
|
Series,
|
19
|
+
concat,
|
20
|
+
merge,
|
20
21
|
)
|
21
22
|
from pydantic import BaseModel, ConfigDict, field_validator
|
22
|
-
import statsmodels.api as sm
|
23
23
|
|
24
24
|
# noinspection PyProtectedMember
|
25
25
|
from statsmodels.regression.linear_model import RegressionResults
|
26
26
|
|
27
27
|
from openseries.common_model import CommonModel
|
28
|
-
from openseries.series import OpenTimeSeries
|
29
28
|
from openseries.datefixer import (
|
30
29
|
align_dataframe_to_local_cdays,
|
31
30
|
do_resample_to_business_period_ends,
|
32
31
|
get_calc_range,
|
33
32
|
)
|
33
|
+
from openseries.risk import (
|
34
|
+
drawdown_details,
|
35
|
+
ewma_calc,
|
36
|
+
)
|
37
|
+
from openseries.series import OpenTimeSeries
|
34
38
|
from openseries.types import (
|
35
39
|
CountriesType,
|
36
|
-
LiteralHowMerge,
|
37
40
|
LiteralBizDayFreq,
|
38
|
-
LiteralPandasResampleConvention,
|
39
|
-
LiteralPandasReindexMethod,
|
40
41
|
LiteralCaptureRatio,
|
42
|
+
LiteralCovMethod,
|
41
43
|
LiteralFrameProps,
|
42
|
-
|
44
|
+
LiteralHowMerge,
|
43
45
|
LiteralOlsFitCovType,
|
46
|
+
LiteralOlsFitMethod,
|
47
|
+
LiteralPandasReindexMethod,
|
48
|
+
LiteralPandasResampleConvention,
|
44
49
|
LiteralPortfolioWeightings,
|
45
|
-
LiteralCovMethod,
|
46
50
|
LiteralRiskParityMethod,
|
47
51
|
OpenFramePropertiesList,
|
48
52
|
ValueType,
|
49
53
|
)
|
50
|
-
from openseries.risk import (
|
51
|
-
drawdown_details,
|
52
|
-
ewma_calc,
|
53
|
-
)
|
54
54
|
|
55
55
|
TypeOpenFrame = TypeVar("TypeOpenFrame", bound="OpenFrame")
|
56
56
|
|
57
57
|
|
58
|
-
class OpenFrame(BaseModel, CommonModel):
|
59
|
-
|
58
|
+
class OpenFrame(BaseModel, CommonModel): # type: ignore[misc]
|
59
|
+
|
60
|
+
"""
|
61
|
+
Object of the class OpenFrame.
|
60
62
|
|
61
63
|
Parameters
|
62
64
|
----------
|
@@ -81,11 +83,12 @@ class OpenFrame(BaseModel, CommonModel):
|
|
81
83
|
revalidate_instances="always",
|
82
84
|
)
|
83
85
|
|
84
|
-
@field_validator("constituents")
|
85
|
-
def check_labels_unique(
|
86
|
-
cls: TypeOpenFrame,
|
86
|
+
@field_validator("constituents") # type: ignore[misc]
|
87
|
+
def check_labels_unique(
|
88
|
+
cls: TypeOpenFrame, # noqa: N805
|
89
|
+
tseries: list[OpenTimeSeries],
|
87
90
|
) -> list[OpenTimeSeries]:
|
88
|
-
"""Pydantic validator ensuring that OpenFrame labels are unique"""
|
91
|
+
"""Pydantic validator ensuring that OpenFrame labels are unique."""
|
89
92
|
labls = [x.label for x in tseries]
|
90
93
|
if len(set(labls)) != len(labls):
|
91
94
|
raise ValueError("TimeSeries names/labels must be unique")
|
@@ -96,6 +99,21 @@ class OpenFrame(BaseModel, CommonModel):
|
|
96
99
|
constituents: list[OpenTimeSeries],
|
97
100
|
weights: Optional[list[float]] = None,
|
98
101
|
) -> None:
|
102
|
+
"""
|
103
|
+
Object of the class OpenFrame.
|
104
|
+
|
105
|
+
Parameters
|
106
|
+
----------
|
107
|
+
constituents: list[TypeOpenTimeSeries]
|
108
|
+
List of objects of Class OpenTimeSeries
|
109
|
+
weights: list[float], optional
|
110
|
+
List of weights in float format.
|
111
|
+
|
112
|
+
Returns
|
113
|
+
-------
|
114
|
+
OpenFrame
|
115
|
+
Object of the class OpenFrame
|
116
|
+
"""
|
99
117
|
super().__init__(constituents=constituents, weights=weights)
|
100
118
|
|
101
119
|
self.constituents = constituents
|
@@ -111,20 +129,22 @@ class OpenFrame(BaseModel, CommonModel):
|
|
111
129
|
warning("OpenFrame() was passed an empty list.")
|
112
130
|
|
113
131
|
def from_deepcopy(self: TypeOpenFrame) -> TypeOpenFrame:
|
114
|
-
"""
|
132
|
+
"""
|
133
|
+
Create copy of the OpenFrame object.
|
115
134
|
|
116
135
|
Returns
|
117
136
|
-------
|
118
137
|
OpenFrame
|
119
138
|
An OpenFrame object
|
120
139
|
"""
|
121
|
-
|
122
140
|
return deepcopy(self)
|
123
141
|
|
124
142
|
def merge_series(
|
125
|
-
self: TypeOpenFrame,
|
143
|
+
self: TypeOpenFrame,
|
144
|
+
how: LiteralHowMerge = "outer",
|
126
145
|
) -> TypeOpenFrame:
|
127
|
-
"""
|
146
|
+
"""
|
147
|
+
Merge index of Pandas Dataframes of the constituent OpenTimeSeries.
|
128
148
|
|
129
149
|
Parameters
|
130
150
|
----------
|
@@ -136,7 +156,6 @@ class OpenFrame(BaseModel, CommonModel):
|
|
136
156
|
OpenFrame
|
137
157
|
An OpenFrame object
|
138
158
|
"""
|
139
|
-
|
140
159
|
self.tsdf = reduce(
|
141
160
|
lambda left, right: merge(
|
142
161
|
left=left,
|
@@ -150,7 +169,7 @@ class OpenFrame(BaseModel, CommonModel):
|
|
150
169
|
if self.tsdf.empty:
|
151
170
|
raise ValueError(
|
152
171
|
f"Merging OpenTimeSeries DataFrames with "
|
153
|
-
f"argument how={how} produced an empty DataFrame."
|
172
|
+
f"argument how={how} produced an empty DataFrame.",
|
154
173
|
)
|
155
174
|
if how == "inner":
|
156
175
|
for xerie in self.constituents:
|
@@ -158,9 +177,11 @@ class OpenFrame(BaseModel, CommonModel):
|
|
158
177
|
return self
|
159
178
|
|
160
179
|
def all_properties(
|
161
|
-
self: TypeOpenFrame,
|
180
|
+
self: TypeOpenFrame,
|
181
|
+
properties: Optional[list[LiteralFrameProps]] = None,
|
162
182
|
) -> DataFrame:
|
163
|
-
"""
|
183
|
+
"""
|
184
|
+
Calculate chosen timeseries properties.
|
164
185
|
|
165
186
|
Parameters
|
166
187
|
----------
|
@@ -188,7 +209,8 @@ class OpenFrame(BaseModel, CommonModel):
|
|
188
209
|
from_dt: Optional[dt.date] = None,
|
189
210
|
to_dt: Optional[dt.date] = None,
|
190
211
|
) -> tuple[dt.date, dt.date]:
|
191
|
-
"""
|
212
|
+
"""
|
213
|
+
Create user defined date range.
|
192
214
|
|
193
215
|
Parameters
|
194
216
|
----------
|
@@ -206,14 +228,18 @@ class OpenFrame(BaseModel, CommonModel):
|
|
206
228
|
Start and end date of the chosen date range
|
207
229
|
"""
|
208
230
|
return get_calc_range(
|
209
|
-
data=self.tsdf,
|
231
|
+
data=self.tsdf,
|
232
|
+
months_offset=months_offset,
|
233
|
+
from_dt=from_dt,
|
234
|
+
to_dt=to_dt,
|
210
235
|
)
|
211
236
|
|
212
237
|
def align_index_to_local_cdays(
|
213
|
-
self: TypeOpenFrame,
|
238
|
+
self: TypeOpenFrame,
|
239
|
+
countries: CountriesType = "SE",
|
214
240
|
) -> TypeOpenFrame:
|
215
|
-
"""
|
216
|
-
local calendar business days
|
241
|
+
"""
|
242
|
+
Align the index of .tsdf with local calendar business days.
|
217
243
|
|
218
244
|
Returns
|
219
245
|
-------
|
@@ -226,12 +252,13 @@ class OpenFrame(BaseModel, CommonModel):
|
|
226
252
|
@property
|
227
253
|
def lengths_of_items(self: TypeOpenFrame) -> Series:
|
228
254
|
"""
|
255
|
+
Number of observations of all constituents.
|
256
|
+
|
229
257
|
Returns
|
230
258
|
-------
|
231
259
|
Pandas.Series
|
232
260
|
Number of observations of all constituents
|
233
261
|
"""
|
234
|
-
|
235
262
|
return Series(
|
236
263
|
data=[self.tsdf.loc[:, d].count() for d in self.tsdf],
|
237
264
|
index=self.tsdf.columns,
|
@@ -242,72 +269,83 @@ class OpenFrame(BaseModel, CommonModel):
|
|
242
269
|
@property
|
243
270
|
def item_count(self: TypeOpenFrame) -> int:
|
244
271
|
"""
|
272
|
+
Number of constituents.
|
273
|
+
|
245
274
|
Returns
|
246
275
|
-------
|
247
276
|
int
|
248
277
|
Number of constituents
|
249
278
|
"""
|
250
|
-
|
251
279
|
return len(self.constituents)
|
252
280
|
|
253
281
|
@property
|
254
282
|
def columns_lvl_zero(self: TypeOpenFrame) -> list[str]:
|
255
283
|
"""
|
284
|
+
Level 0 values of the MultiIndex columns in the .tsdf DataFrame.
|
285
|
+
|
256
286
|
Returns
|
257
287
|
-------
|
258
288
|
list[str]
|
259
|
-
Level 0 values of the
|
260
|
-
Pandas.DataFrame
|
289
|
+
Level 0 values of the MultiIndex columns in the .tsdf DataFrame
|
261
290
|
"""
|
262
|
-
|
263
291
|
return list(self.tsdf.columns.get_level_values(0))
|
264
292
|
|
265
293
|
@property
|
266
294
|
def columns_lvl_one(self: TypeOpenFrame) -> list[str]:
|
267
295
|
"""
|
296
|
+
Level 1 values of the MultiIndex columns in the .tsdf DataFrame.
|
297
|
+
|
268
298
|
Returns
|
269
299
|
-------
|
270
300
|
list[str]
|
271
|
-
Level 1 values of the
|
272
|
-
Pandas.DataFrame
|
301
|
+
Level 1 values of the MultiIndex columns in the .tsdf DataFrame
|
273
302
|
"""
|
274
|
-
|
275
303
|
return list(self.tsdf.columns.get_level_values(1))
|
276
304
|
|
277
305
|
@property
|
278
306
|
def first_indices(self: TypeOpenFrame) -> Series:
|
279
307
|
"""
|
308
|
+
The first dates in the timeseries of all constituents.
|
309
|
+
|
280
310
|
Returns
|
281
311
|
-------
|
282
312
|
Pandas.Series
|
283
313
|
The first dates in the timeseries of all constituents
|
284
314
|
"""
|
285
|
-
|
286
315
|
return Series(
|
287
316
|
data=[i.first_idx for i in self.constituents],
|
288
317
|
index=self.tsdf.columns,
|
289
318
|
name="first indices",
|
290
|
-
|
319
|
+
dtype="datetime64[ns]",
|
320
|
+
).dt.date
|
291
321
|
|
292
322
|
@property
|
293
323
|
def last_indices(self: TypeOpenFrame) -> Series:
|
294
324
|
"""
|
325
|
+
The last dates in the timeseries of all constituents.
|
326
|
+
|
295
327
|
Returns
|
296
328
|
-------
|
297
329
|
Pandas.Series
|
298
330
|
The last dates in the timeseries of all constituents
|
299
331
|
"""
|
300
|
-
|
301
332
|
return Series(
|
302
333
|
data=[i.last_idx for i in self.constituents],
|
303
334
|
index=self.tsdf.columns,
|
304
335
|
name="last indices",
|
305
|
-
|
336
|
+
dtype="datetime64[ns]",
|
337
|
+
).dt.date
|
306
338
|
|
307
339
|
@property
|
308
340
|
def span_of_days_all(self: TypeOpenFrame) -> Series:
|
309
341
|
"""
|
310
342
|
Number of days from the first date to the last for all items in the frame.
|
343
|
+
|
344
|
+
Returns
|
345
|
+
-------
|
346
|
+
Pandas.Series
|
347
|
+
Number of days from the first date to the last for all
|
348
|
+
items in the frame.
|
311
349
|
"""
|
312
350
|
return Series(
|
313
351
|
data=[c.span_of_days for c in self.constituents],
|
@@ -322,12 +360,15 @@ class OpenFrame(BaseModel, CommonModel):
|
|
322
360
|
market: Union[tuple[str, ValueType], int],
|
323
361
|
riskfree_rate: float = 0.0,
|
324
362
|
) -> float:
|
325
|
-
"""
|
363
|
+
"""
|
364
|
+
Jensen's alpha.
|
365
|
+
|
366
|
+
The Jensen's measure, or Jensen's alpha, is a risk-adjusted performance
|
326
367
|
measure that represents the average return on a portfolio or investment,
|
327
368
|
above or below that predicted by the capital asset pricing model (CAPM),
|
328
369
|
given the portfolio's or investment's beta and the average market return.
|
329
370
|
This metric is also commonly referred to as simply alpha.
|
330
|
-
https://www.investopedia.com/terms/j/jensensmeasure.asp
|
371
|
+
https://www.investopedia.com/terms/j/jensensmeasure.asp.
|
331
372
|
|
332
373
|
Parameters
|
333
374
|
----------
|
@@ -344,7 +385,8 @@ class OpenFrame(BaseModel, CommonModel):
|
|
344
385
|
Jensen's alpha
|
345
386
|
"""
|
346
387
|
if all(
|
347
|
-
x == ValueType.RTRN
|
388
|
+
x == ValueType.RTRN
|
389
|
+
for x in self.tsdf.columns.get_level_values(1).to_numpy()
|
348
390
|
):
|
349
391
|
if isinstance(asset, tuple):
|
350
392
|
asset_log = self.tsdf.loc[:, asset]
|
@@ -354,7 +396,7 @@ class OpenFrame(BaseModel, CommonModel):
|
|
354
396
|
asset_cagr = asset_log.mean()
|
355
397
|
else:
|
356
398
|
raise ValueError(
|
357
|
-
"asset should be a tuple[str, ValueType] or an integer."
|
399
|
+
"asset should be a tuple[str, ValueType] or an integer.",
|
358
400
|
)
|
359
401
|
if isinstance(market, tuple):
|
360
402
|
market_log = self.tsdf.loc[:, market]
|
@@ -364,12 +406,12 @@ class OpenFrame(BaseModel, CommonModel):
|
|
364
406
|
market_cagr = market_log.mean()
|
365
407
|
else:
|
366
408
|
raise ValueError(
|
367
|
-
"market should be a tuple[str, ValueType] or an integer."
|
409
|
+
"market should be a tuple[str, ValueType] or an integer.",
|
368
410
|
)
|
369
411
|
else:
|
370
412
|
if isinstance(asset, tuple):
|
371
413
|
asset_log = log(
|
372
|
-
self.tsdf.loc[:, asset] / self.tsdf.loc[:, asset].iloc[0]
|
414
|
+
self.tsdf.loc[:, asset] / self.tsdf.loc[:, asset].iloc[0],
|
373
415
|
)
|
374
416
|
if self.yearfrac > 1.0:
|
375
417
|
asset_cagr = (
|
@@ -394,11 +436,11 @@ class OpenFrame(BaseModel, CommonModel):
|
|
394
436
|
)
|
395
437
|
else:
|
396
438
|
raise ValueError(
|
397
|
-
"asset should be a tuple[str, ValueType] or an integer."
|
439
|
+
"asset should be a tuple[str, ValueType] or an integer.",
|
398
440
|
)
|
399
441
|
if isinstance(market, tuple):
|
400
442
|
market_log = log(
|
401
|
-
self.tsdf.loc[:, market] / self.tsdf.loc[:, market].iloc[0]
|
443
|
+
self.tsdf.loc[:, market] / self.tsdf.loc[:, market].iloc[0],
|
402
444
|
)
|
403
445
|
if self.yearfrac > 1.0:
|
404
446
|
market_cagr = (
|
@@ -423,7 +465,7 @@ class OpenFrame(BaseModel, CommonModel):
|
|
423
465
|
)
|
424
466
|
else:
|
425
467
|
raise ValueError(
|
426
|
-
"market should be a tuple[str, ValueType] or an integer."
|
468
|
+
"market should be a tuple[str, ValueType] or an integer.",
|
427
469
|
)
|
428
470
|
|
429
471
|
covariance = cov(asset_log, market_log, ddof=1)
|
@@ -434,12 +476,13 @@ class OpenFrame(BaseModel, CommonModel):
|
|
434
476
|
@property
|
435
477
|
def worst_month(self: TypeOpenFrame) -> Series:
|
436
478
|
"""
|
479
|
+
Most negative month.
|
480
|
+
|
437
481
|
Returns
|
438
482
|
-------
|
439
483
|
Pandas.Series
|
440
484
|
Most negative month
|
441
485
|
"""
|
442
|
-
|
443
486
|
wdf = self.tsdf.copy()
|
444
487
|
wdf.index = DatetimeIndex(wdf.index)
|
445
488
|
return Series(
|
@@ -450,12 +493,13 @@ class OpenFrame(BaseModel, CommonModel):
|
|
450
493
|
|
451
494
|
def value_to_ret(self: TypeOpenFrame) -> TypeOpenFrame:
|
452
495
|
"""
|
496
|
+
Convert series of values into series of returns.
|
497
|
+
|
453
498
|
Returns
|
454
499
|
-------
|
455
500
|
OpenFrame
|
456
501
|
The returns of the values in the series
|
457
502
|
"""
|
458
|
-
|
459
503
|
self.tsdf = self.tsdf.pct_change()
|
460
504
|
self.tsdf.iloc[0] = 0
|
461
505
|
new_labels = [ValueType.RTRN] * self.item_count
|
@@ -464,7 +508,8 @@ class OpenFrame(BaseModel, CommonModel):
|
|
464
508
|
return self
|
465
509
|
|
466
510
|
def value_to_diff(self: TypeOpenFrame, periods: int = 1) -> TypeOpenFrame:
|
467
|
-
"""
|
511
|
+
"""
|
512
|
+
Convert series of values to series of their period differences.
|
468
513
|
|
469
514
|
Parameters
|
470
515
|
----------
|
@@ -477,7 +522,6 @@ class OpenFrame(BaseModel, CommonModel):
|
|
477
522
|
OpenFrame
|
478
523
|
An OpenFrame object
|
479
524
|
"""
|
480
|
-
|
481
525
|
self.tsdf = self.tsdf.diff(periods=periods)
|
482
526
|
self.tsdf.iloc[0] = 0
|
483
527
|
new_labels = [ValueType.RTRN] * self.item_count
|
@@ -486,7 +530,8 @@ class OpenFrame(BaseModel, CommonModel):
|
|
486
530
|
return self
|
487
531
|
|
488
532
|
def to_cumret(self: TypeOpenFrame) -> TypeOpenFrame:
|
489
|
-
"""
|
533
|
+
"""
|
534
|
+
Convert series of returns into cumulative series of values.
|
490
535
|
|
491
536
|
Returns
|
492
537
|
-------
|
@@ -494,7 +539,8 @@ class OpenFrame(BaseModel, CommonModel):
|
|
494
539
|
An OpenFrame object
|
495
540
|
"""
|
496
541
|
if any(
|
497
|
-
x == ValueType.PRICE
|
542
|
+
x == ValueType.PRICE
|
543
|
+
for x in self.tsdf.columns.get_level_values(1).to_numpy()
|
498
544
|
):
|
499
545
|
self.value_to_ret()
|
500
546
|
|
@@ -506,9 +552,11 @@ class OpenFrame(BaseModel, CommonModel):
|
|
506
552
|
return self
|
507
553
|
|
508
554
|
def resample(
|
509
|
-
self: TypeOpenFrame,
|
555
|
+
self: TypeOpenFrame,
|
556
|
+
freq: Union[LiteralBizDayFreq, str] = "BM",
|
510
557
|
) -> TypeOpenFrame:
|
511
|
-
"""
|
558
|
+
"""
|
559
|
+
Resample the timeseries frequency.
|
512
560
|
|
513
561
|
Parameters
|
514
562
|
----------
|
@@ -521,7 +569,6 @@ class OpenFrame(BaseModel, CommonModel):
|
|
521
569
|
OpenFrame
|
522
570
|
An OpenFrame object
|
523
571
|
"""
|
524
|
-
|
525
572
|
self.tsdf.index = DatetimeIndex(self.tsdf.index)
|
526
573
|
self.tsdf = self.tsdf.resample(freq).last()
|
527
574
|
self.tsdf.index = [d.date() for d in DatetimeIndex(self.tsdf.index)]
|
@@ -541,15 +588,16 @@ class OpenFrame(BaseModel, CommonModel):
|
|
541
588
|
convention: LiteralPandasResampleConvention = "end",
|
542
589
|
method: LiteralPandasReindexMethod = "nearest",
|
543
590
|
) -> TypeOpenFrame:
|
544
|
-
"""
|
545
|
-
|
546
|
-
|
591
|
+
"""
|
592
|
+
Resamples timeseries frequency to the business calendar month end dates.
|
593
|
+
|
594
|
+
Stubs left in place. Stubs will be aligned to the shortest stub.
|
547
595
|
|
548
596
|
Parameters
|
549
597
|
----------
|
550
598
|
freq: LiteralBizDayFreq, default BM
|
551
599
|
The date offset string that sets the resampled frequency
|
552
|
-
countries:
|
600
|
+
countries: CountriesType, default: "SE"
|
553
601
|
(List of) country code(s) according to ISO 3166-1 alpha-2
|
554
602
|
to create a business day calendar used for date adjustments
|
555
603
|
convention: LiteralPandasResampleConvention, default; end
|
@@ -562,7 +610,6 @@ class OpenFrame(BaseModel, CommonModel):
|
|
562
610
|
OpenFrame
|
563
611
|
An OpenFrame object
|
564
612
|
"""
|
565
|
-
|
566
613
|
head = self.tsdf.loc[self.first_indices.max()].copy()
|
567
614
|
tail = self.tsdf.loc[self.last_indices.min()].copy()
|
568
615
|
dates = do_resample_to_business_period_ends(
|
@@ -576,13 +623,14 @@ class OpenFrame(BaseModel, CommonModel):
|
|
576
623
|
self.tsdf = self.tsdf.reindex([deyt.date() for deyt in dates], method=method)
|
577
624
|
for xerie in self.constituents:
|
578
625
|
xerie.tsdf = xerie.tsdf.reindex(
|
579
|
-
[deyt.date() for deyt in dates],
|
626
|
+
[deyt.date() for deyt in dates],
|
627
|
+
method=method,
|
580
628
|
)
|
581
629
|
return self
|
582
630
|
|
583
631
|
def drawdown_details(self: TypeOpenFrame, min_periods: int = 1) -> DataFrame:
|
584
|
-
"""
|
585
|
-
|
632
|
+
"""
|
633
|
+
Details of the maximum drawdown.
|
586
634
|
|
587
635
|
Parameters
|
588
636
|
----------
|
@@ -592,9 +640,12 @@ class OpenFrame(BaseModel, CommonModel):
|
|
592
640
|
Returns
|
593
641
|
-------
|
594
642
|
Pandas.DataFrame
|
595
|
-
Drawdown
|
643
|
+
Max Drawdown
|
644
|
+
Start of drawdown
|
645
|
+
Date of bottom
|
646
|
+
Days from start to bottom
|
647
|
+
Average fall per day
|
596
648
|
"""
|
597
|
-
|
598
649
|
mxdwndf = DataFrame()
|
599
650
|
for i in self.constituents:
|
600
651
|
tmpdf = i.tsdf.copy()
|
@@ -616,15 +667,17 @@ class OpenFrame(BaseModel, CommonModel):
|
|
616
667
|
to_date: Optional[dt.date] = None,
|
617
668
|
periods_in_a_year_fixed: Optional[int] = None,
|
618
669
|
) -> DataFrame:
|
619
|
-
"""
|
620
|
-
Correlation.
|
621
|
-
|
670
|
+
"""
|
671
|
+
Exponentially Weighted Moving Average Volatilities and Correlation.
|
672
|
+
|
673
|
+
Exponentially Weighted Moving Average (EWMA) for Volatilities and
|
674
|
+
Correlation. https://www.investopedia.com/articles/07/ewma.asp.
|
622
675
|
|
623
676
|
Parameters
|
624
677
|
----------
|
625
678
|
lmbda: float, default: 0.94
|
626
679
|
Scaling factor to determine weighting.
|
627
|
-
day_chunk: int, default:
|
680
|
+
day_chunk: int, default: 11
|
628
681
|
Sampling the data which is assumed to be daily.
|
629
682
|
dlta_degr_freedms: int, default: 0
|
630
683
|
Variance bias factor taking the value 0 or 1.
|
@@ -648,7 +701,6 @@ class OpenFrame(BaseModel, CommonModel):
|
|
648
701
|
Pandas.DataFrame
|
649
702
|
Series volatilities and correlation
|
650
703
|
"""
|
651
|
-
|
652
704
|
earlier, later = self.calc_range(months_from_last, from_date, to_date)
|
653
705
|
if periods_in_a_year_fixed is None:
|
654
706
|
fraction = (later - earlier).days / 365.25
|
@@ -680,20 +732,20 @@ class OpenFrame(BaseModel, CommonModel):
|
|
680
732
|
data.loc[:, (cols[0], "Returns")]
|
681
733
|
.iloc[1:day_chunk]
|
682
734
|
.std(ddof=dlta_degr_freedms)
|
683
|
-
* sqrt(time_factor)
|
735
|
+
* sqrt(time_factor),
|
684
736
|
]
|
685
737
|
raw_two = [
|
686
738
|
data.loc[:, (cols[1], "Returns")]
|
687
739
|
.iloc[1:day_chunk]
|
688
740
|
.std(ddof=dlta_degr_freedms)
|
689
|
-
* sqrt(time_factor)
|
741
|
+
* sqrt(time_factor),
|
690
742
|
]
|
691
743
|
raw_cov = [
|
692
744
|
cov(
|
693
745
|
m=data.loc[:, (cols[0], "Returns")].iloc[1:day_chunk].to_numpy(),
|
694
746
|
y=data.loc[:, (cols[1], "Returns")].iloc[1:day_chunk].to_numpy(),
|
695
747
|
ddof=dlta_degr_freedms,
|
696
|
-
)[0][1]
|
748
|
+
)[0][1],
|
697
749
|
]
|
698
750
|
raw_corr = [raw_cov[0] / (2 * raw_one[0] * raw_two[0])]
|
699
751
|
|
@@ -724,7 +776,7 @@ class OpenFrame(BaseModel, CommonModel):
|
|
724
776
|
raw_corr.append(tmp_raw_corr)
|
725
777
|
|
726
778
|
return DataFrame(
|
727
|
-
index=cols
|
779
|
+
index=[*cols, corr_label],
|
728
780
|
columns=data.index,
|
729
781
|
data=[raw_one, raw_two, raw_corr],
|
730
782
|
).T
|
@@ -732,6 +784,8 @@ class OpenFrame(BaseModel, CommonModel):
|
|
732
784
|
@property
|
733
785
|
def correl_matrix(self: TypeOpenFrame) -> DataFrame:
|
734
786
|
"""
|
787
|
+
Correlation matrix.
|
788
|
+
|
735
789
|
Returns
|
736
790
|
-------
|
737
791
|
Pandas.DataFrame
|
@@ -744,9 +798,12 @@ class OpenFrame(BaseModel, CommonModel):
|
|
744
798
|
return corr_matrix
|
745
799
|
|
746
800
|
def add_timeseries(
|
747
|
-
self: TypeOpenFrame,
|
801
|
+
self: TypeOpenFrame,
|
802
|
+
new_series: OpenTimeSeries,
|
748
803
|
) -> TypeOpenFrame:
|
749
804
|
"""
|
805
|
+
To add an OpenTimeSeries object.
|
806
|
+
|
750
807
|
Parameters
|
751
808
|
----------
|
752
809
|
new_series: OpenTimeSeries
|
@@ -763,6 +820,8 @@ class OpenFrame(BaseModel, CommonModel):
|
|
763
820
|
|
764
821
|
def delete_timeseries(self: TypeOpenFrame, lvl_zero_item: str) -> TypeOpenFrame:
|
765
822
|
"""
|
823
|
+
To delete an OpenTimeSeries object.
|
824
|
+
|
766
825
|
Parameters
|
767
826
|
----------
|
768
827
|
lvl_zero_item: str
|
@@ -785,7 +844,7 @@ class OpenFrame(BaseModel, CommonModel):
|
|
785
844
|
self.constituents = [
|
786
845
|
item for item in self.constituents if item.label != lvl_zero_item
|
787
846
|
]
|
788
|
-
self.tsdf.drop(lvl_zero_item, axis="columns", level=0
|
847
|
+
self.tsdf = self.tsdf.drop(lvl_zero_item, axis="columns", level=0)
|
789
848
|
return self
|
790
849
|
|
791
850
|
def trunc_frame(
|
@@ -795,7 +854,8 @@ class OpenFrame(BaseModel, CommonModel):
|
|
795
854
|
before: bool = True,
|
796
855
|
after: bool = True,
|
797
856
|
) -> TypeOpenFrame:
|
798
|
-
"""
|
857
|
+
"""
|
858
|
+
Truncate DataFrame such that all timeseries have the same time span.
|
799
859
|
|
800
860
|
Parameters
|
801
861
|
----------
|
@@ -815,29 +875,30 @@ class OpenFrame(BaseModel, CommonModel):
|
|
815
875
|
OpenFrame
|
816
876
|
An OpenFrame object
|
817
877
|
"""
|
818
|
-
|
819
878
|
if not start_cut and before:
|
820
879
|
start_cut = self.first_indices.max()
|
821
880
|
if not end_cut and after:
|
822
881
|
end_cut = self.last_indices.min()
|
823
|
-
self.tsdf.sort_index(
|
882
|
+
self.tsdf = self.tsdf.sort_index()
|
824
883
|
self.tsdf = self.tsdf.truncate(before=start_cut, after=end_cut, copy=False)
|
825
884
|
|
826
885
|
for xerie in self.constituents:
|
827
886
|
xerie.tsdf = xerie.tsdf.truncate(
|
828
|
-
before=start_cut,
|
887
|
+
before=start_cut,
|
888
|
+
after=end_cut,
|
889
|
+
copy=False,
|
829
890
|
)
|
830
891
|
if len(set(self.first_indices)) != 1:
|
831
892
|
warning(
|
832
893
|
f"One or more constituents still not truncated to same "
|
833
894
|
f"start dates.\n"
|
834
|
-
f"{self.tsdf.head()}"
|
895
|
+
f"{self.tsdf.head()}",
|
835
896
|
)
|
836
897
|
if len(set(self.last_indices)) != 1:
|
837
898
|
warning(
|
838
899
|
f"One or more constituents still not truncated to same "
|
839
900
|
f"end dates.\n"
|
840
|
-
f"{self.tsdf.tail()}"
|
901
|
+
f"{self.tsdf.tail()}",
|
841
902
|
)
|
842
903
|
return self
|
843
904
|
|
@@ -847,8 +908,8 @@ class OpenFrame(BaseModel, CommonModel):
|
|
847
908
|
short_column: int = 1,
|
848
909
|
base_zero: bool = True,
|
849
910
|
) -> None:
|
850
|
-
"""
|
851
|
-
|
911
|
+
"""
|
912
|
+
Calculate cumulative relative return between two series.
|
852
913
|
|
853
914
|
Parameters
|
854
915
|
----------
|
@@ -860,7 +921,6 @@ class OpenFrame(BaseModel, CommonModel):
|
|
860
921
|
If set to False 1.0 is added to allow for a capital base and
|
861
922
|
to allow a volatility calculation
|
862
923
|
"""
|
863
|
-
|
864
924
|
rel_label = (
|
865
925
|
self.tsdf.iloc[:, long_column].name[0]
|
866
926
|
+ "_over_"
|
@@ -883,9 +943,12 @@ class OpenFrame(BaseModel, CommonModel):
|
|
883
943
|
to_date: Optional[dt.date] = None,
|
884
944
|
periods_in_a_year_fixed: Optional[int] = None,
|
885
945
|
) -> Series:
|
886
|
-
"""
|
887
|
-
|
888
|
-
|
946
|
+
"""
|
947
|
+
Tracking Error.
|
948
|
+
|
949
|
+
Calculates Tracking Error which is the standard deviation of the
|
950
|
+
difference between the fund and its index returns.
|
951
|
+
https://www.investopedia.com/terms/t/trackingerror.asp.
|
889
952
|
|
890
953
|
Parameters
|
891
954
|
----------
|
@@ -907,25 +970,26 @@ class OpenFrame(BaseModel, CommonModel):
|
|
907
970
|
Pandas.Series
|
908
971
|
Tracking Errors
|
909
972
|
"""
|
910
|
-
|
911
973
|
earlier, later = self.calc_range(months_from_last, from_date, to_date)
|
912
974
|
fraction = (later - earlier).days / 365.25
|
913
975
|
|
914
976
|
if isinstance(base_column, tuple):
|
915
977
|
shortdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].loc[
|
916
|
-
:,
|
978
|
+
:,
|
979
|
+
base_column,
|
917
980
|
]
|
918
981
|
short_item = base_column
|
919
982
|
short_label = self.tsdf.loc[:, base_column].name[0]
|
920
983
|
elif isinstance(base_column, int):
|
921
984
|
shortdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].iloc[
|
922
|
-
:,
|
985
|
+
:,
|
986
|
+
base_column,
|
923
987
|
]
|
924
988
|
short_item = self.tsdf.iloc[:, base_column].name
|
925
989
|
short_label = self.tsdf.iloc[:, base_column].name[0]
|
926
990
|
else:
|
927
991
|
raise ValueError(
|
928
|
-
"base_column should be a tuple[str, ValueType] or an integer."
|
992
|
+
"base_column should be a tuple[str, ValueType] or an integer.",
|
929
993
|
)
|
930
994
|
|
931
995
|
if periods_in_a_year_fixed:
|
@@ -939,7 +1003,8 @@ class OpenFrame(BaseModel, CommonModel):
|
|
939
1003
|
terrors.append(0.0)
|
940
1004
|
else:
|
941
1005
|
longdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].loc[
|
942
|
-
:,
|
1006
|
+
:,
|
1007
|
+
item,
|
943
1008
|
]
|
944
1009
|
relative = 1.0 + longdf - shortdf
|
945
1010
|
vol = float(relative.pct_change().std() * sqrt(time_factor))
|
@@ -960,7 +1025,10 @@ class OpenFrame(BaseModel, CommonModel):
|
|
960
1025
|
to_date: Optional[dt.date] = None,
|
961
1026
|
periods_in_a_year_fixed: Optional[int] = None,
|
962
1027
|
) -> Series:
|
963
|
-
"""
|
1028
|
+
"""
|
1029
|
+
Information Ratio.
|
1030
|
+
|
1031
|
+
The Information Ratio equals ( fund return less index return ) divided
|
964
1032
|
by the Tracking Error. And the Tracking Error is the standard deviation of
|
965
1033
|
the difference between the fund and its index returns.
|
966
1034
|
The ratio is calculated using the annualized arithmetic mean of returns.
|
@@ -985,25 +1053,26 @@ class OpenFrame(BaseModel, CommonModel):
|
|
985
1053
|
Pandas.Series
|
986
1054
|
Information Ratios
|
987
1055
|
"""
|
988
|
-
|
989
1056
|
earlier, later = self.calc_range(months_from_last, from_date, to_date)
|
990
1057
|
fraction = (later - earlier).days / 365.25
|
991
1058
|
|
992
1059
|
if isinstance(base_column, tuple):
|
993
1060
|
shortdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].loc[
|
994
|
-
:,
|
1061
|
+
:,
|
1062
|
+
base_column,
|
995
1063
|
]
|
996
1064
|
short_item = base_column
|
997
1065
|
short_label = self.tsdf.loc[:, base_column].name[0]
|
998
1066
|
elif isinstance(base_column, int):
|
999
1067
|
shortdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].iloc[
|
1000
|
-
:,
|
1068
|
+
:,
|
1069
|
+
base_column,
|
1001
1070
|
]
|
1002
1071
|
short_item = self.tsdf.iloc[:, base_column].name
|
1003
1072
|
short_label = self.tsdf.iloc[:, base_column].name[0]
|
1004
1073
|
else:
|
1005
1074
|
raise ValueError(
|
1006
|
-
"base_column should be a tuple[str, ValueType] or an integer."
|
1075
|
+
"base_column should be a tuple[str, ValueType] or an integer.",
|
1007
1076
|
)
|
1008
1077
|
|
1009
1078
|
if periods_in_a_year_fixed:
|
@@ -1017,7 +1086,8 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1017
1086
|
ratios.append(0.0)
|
1018
1087
|
else:
|
1019
1088
|
longdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].loc[
|
1020
|
-
:,
|
1089
|
+
:,
|
1090
|
+
item,
|
1021
1091
|
]
|
1022
1092
|
relative = 1.0 + longdf - shortdf
|
1023
1093
|
ret = float(relative.pct_change().mean() * time_factor)
|
@@ -1040,14 +1110,17 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1040
1110
|
to_date: Optional[dt.date] = None,
|
1041
1111
|
periods_in_a_year_fixed: Optional[int] = None,
|
1042
1112
|
) -> Series:
|
1043
|
-
"""
|
1113
|
+
"""
|
1114
|
+
Capture Ratio.
|
1115
|
+
|
1116
|
+
The Up (Down) Capture Ratio is calculated by dividing the CAGR
|
1044
1117
|
of the asset during periods that the benchmark returns are positive (negative)
|
1045
1118
|
by the CAGR of the benchmark during the same periods.
|
1046
1119
|
CaptureRatio.BOTH is the Up ratio divided by the Down ratio.
|
1047
1120
|
Source: 'Capture Ratios: A Popular Method of Measuring Portfolio Performance
|
1048
1121
|
in Practice', Don R. Cox and Delbert C. Goff, Journal of Economics and
|
1049
1122
|
Finance Education (Vol 2 Winter 2013).
|
1050
|
-
https://www.economics-finance.org/jefe/volume12-2/11ArticleCox.pdf
|
1123
|
+
https://www.economics-finance.org/jefe/volume12-2/11ArticleCox.pdf.
|
1051
1124
|
|
1052
1125
|
Parameters
|
1053
1126
|
----------
|
@@ -1082,19 +1155,21 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1082
1155
|
|
1083
1156
|
if isinstance(base_column, tuple):
|
1084
1157
|
shortdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].loc[
|
1085
|
-
:,
|
1158
|
+
:,
|
1159
|
+
base_column,
|
1086
1160
|
]
|
1087
1161
|
short_item = base_column
|
1088
1162
|
short_label = self.tsdf.loc[:, base_column].name[0]
|
1089
1163
|
elif isinstance(base_column, int):
|
1090
1164
|
shortdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].iloc[
|
1091
|
-
:,
|
1165
|
+
:,
|
1166
|
+
base_column,
|
1092
1167
|
]
|
1093
1168
|
short_item = self.tsdf.iloc[:, base_column].name
|
1094
1169
|
short_label = self.tsdf.iloc[:, base_column].name[0]
|
1095
1170
|
else:
|
1096
1171
|
raise ValueError(
|
1097
|
-
"base_column should be a tuple[str, ValueType] or an integer."
|
1172
|
+
"base_column should be a tuple[str, ValueType] or an integer.",
|
1098
1173
|
)
|
1099
1174
|
|
1100
1175
|
if periods_in_a_year_fixed:
|
@@ -1108,21 +1183,22 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1108
1183
|
ratios.append(0.0)
|
1109
1184
|
else:
|
1110
1185
|
longdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].loc[
|
1111
|
-
:,
|
1186
|
+
:,
|
1187
|
+
item,
|
1112
1188
|
]
|
1113
1189
|
if ratio == "up":
|
1114
1190
|
uparray = (
|
1115
|
-
longdf.pct_change()[shortdf.pct_change().
|
1191
|
+
longdf.pct_change()[shortdf.pct_change().to_numpy() > 0.0]
|
1116
1192
|
.add(1)
|
1117
|
-
.
|
1193
|
+
.to_numpy()
|
1118
1194
|
)
|
1119
1195
|
up_return = (
|
1120
1196
|
uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
|
1121
1197
|
)
|
1122
1198
|
upidxarray = (
|
1123
|
-
shortdf.pct_change()[shortdf.pct_change().
|
1199
|
+
shortdf.pct_change()[shortdf.pct_change().to_numpy() > 0.0]
|
1124
1200
|
.add(1)
|
1125
|
-
.
|
1201
|
+
.to_numpy()
|
1126
1202
|
)
|
1127
1203
|
up_idx_return = (
|
1128
1204
|
upidxarray.prod() ** (1 / (len(upidxarray) / time_factor)) - 1
|
@@ -1130,17 +1206,17 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1130
1206
|
ratios.append(up_return / up_idx_return)
|
1131
1207
|
elif ratio == "down":
|
1132
1208
|
downarray = (
|
1133
|
-
longdf.pct_change()[shortdf.pct_change().
|
1209
|
+
longdf.pct_change()[shortdf.pct_change().to_numpy() < 0.0]
|
1134
1210
|
.add(1)
|
1135
|
-
.
|
1211
|
+
.to_numpy()
|
1136
1212
|
)
|
1137
1213
|
down_return = (
|
1138
1214
|
downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
|
1139
1215
|
)
|
1140
1216
|
downidxarray = (
|
1141
|
-
shortdf.pct_change()[shortdf.pct_change().
|
1217
|
+
shortdf.pct_change()[shortdf.pct_change().to_numpy() < 0.0]
|
1142
1218
|
.add(1)
|
1143
|
-
.
|
1219
|
+
.to_numpy()
|
1144
1220
|
)
|
1145
1221
|
down_idx_return = (
|
1146
1222
|
downidxarray.prod() ** (1 / (len(downidxarray) / time_factor))
|
@@ -1149,40 +1225,40 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1149
1225
|
ratios.append(down_return / down_idx_return)
|
1150
1226
|
elif ratio == "both":
|
1151
1227
|
uparray = (
|
1152
|
-
longdf.pct_change()[shortdf.pct_change().
|
1228
|
+
longdf.pct_change()[shortdf.pct_change().to_numpy() > 0.0]
|
1153
1229
|
.add(1)
|
1154
|
-
.
|
1230
|
+
.to_numpy()
|
1155
1231
|
)
|
1156
1232
|
up_return = (
|
1157
1233
|
uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
|
1158
1234
|
)
|
1159
1235
|
upidxarray = (
|
1160
|
-
shortdf.pct_change()[shortdf.pct_change().
|
1236
|
+
shortdf.pct_change()[shortdf.pct_change().to_numpy() > 0.0]
|
1161
1237
|
.add(1)
|
1162
|
-
.
|
1238
|
+
.to_numpy()
|
1163
1239
|
)
|
1164
1240
|
up_idx_return = (
|
1165
1241
|
upidxarray.prod() ** (1 / (len(upidxarray) / time_factor)) - 1
|
1166
1242
|
)
|
1167
1243
|
downarray = (
|
1168
|
-
longdf.pct_change()[shortdf.pct_change().
|
1244
|
+
longdf.pct_change()[shortdf.pct_change().to_numpy() < 0.0]
|
1169
1245
|
.add(1)
|
1170
|
-
.
|
1246
|
+
.to_numpy()
|
1171
1247
|
)
|
1172
1248
|
down_return = (
|
1173
1249
|
downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
|
1174
1250
|
)
|
1175
1251
|
downidxarray = (
|
1176
|
-
shortdf.pct_change()[shortdf.pct_change().
|
1252
|
+
shortdf.pct_change()[shortdf.pct_change().to_numpy() < 0.0]
|
1177
1253
|
.add(1)
|
1178
|
-
.
|
1254
|
+
.to_numpy()
|
1179
1255
|
)
|
1180
1256
|
down_idx_return = (
|
1181
1257
|
downidxarray.prod() ** (1 / (len(downidxarray) / time_factor))
|
1182
1258
|
- 1
|
1183
1259
|
)
|
1184
1260
|
ratios.append(
|
1185
|
-
(up_return / up_idx_return) / (down_return / down_idx_return)
|
1261
|
+
(up_return / up_idx_return) / (down_return / down_idx_return),
|
1186
1262
|
)
|
1187
1263
|
|
1188
1264
|
if ratio == "up":
|
@@ -1204,8 +1280,11 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1204
1280
|
asset: Union[tuple[str, ValueType], int],
|
1205
1281
|
market: Union[tuple[str, ValueType], int],
|
1206
1282
|
) -> float:
|
1207
|
-
"""
|
1208
|
-
|
1283
|
+
"""
|
1284
|
+
Market Beta.
|
1285
|
+
|
1286
|
+
Calculates Beta as Co-variance of asset & market divided by Variance
|
1287
|
+
of the market. https://www.investopedia.com/terms/b/beta.asp.
|
1209
1288
|
|
1210
1289
|
Parameters
|
1211
1290
|
----------
|
@@ -1221,7 +1300,7 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1221
1300
|
"""
|
1222
1301
|
if all(
|
1223
1302
|
x_value == ValueType.RTRN
|
1224
|
-
for x_value in self.tsdf.columns.get_level_values(1).
|
1303
|
+
for x_value in self.tsdf.columns.get_level_values(1).to_numpy()
|
1225
1304
|
):
|
1226
1305
|
if isinstance(asset, tuple):
|
1227
1306
|
y_value = self.tsdf.loc[:, asset]
|
@@ -1229,7 +1308,7 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1229
1308
|
y_value = self.tsdf.iloc[:, asset]
|
1230
1309
|
else:
|
1231
1310
|
raise ValueError(
|
1232
|
-
"asset should be a tuple[str, ValueType] or an integer."
|
1311
|
+
"asset should be a tuple[str, ValueType] or an integer.",
|
1233
1312
|
)
|
1234
1313
|
if isinstance(market, tuple):
|
1235
1314
|
x_value = self.tsdf.loc[:, market]
|
@@ -1237,28 +1316,28 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1237
1316
|
x_value = self.tsdf.iloc[:, market]
|
1238
1317
|
else:
|
1239
1318
|
raise ValueError(
|
1240
|
-
"market should be a tuple[str, ValueType] or an integer."
|
1319
|
+
"market should be a tuple[str, ValueType] or an integer.",
|
1241
1320
|
)
|
1242
1321
|
else:
|
1243
1322
|
if isinstance(asset, tuple):
|
1244
1323
|
y_value = log(
|
1245
|
-
self.tsdf.loc[:, asset] / self.tsdf.loc[:, asset].iloc[0]
|
1324
|
+
self.tsdf.loc[:, asset] / self.tsdf.loc[:, asset].iloc[0],
|
1246
1325
|
)
|
1247
1326
|
elif isinstance(asset, int):
|
1248
1327
|
y_value = log(self.tsdf.iloc[:, asset] / self.tsdf.iloc[0, asset])
|
1249
1328
|
else:
|
1250
1329
|
raise ValueError(
|
1251
|
-
"asset should be a tuple[str, ValueType] or an integer."
|
1330
|
+
"asset should be a tuple[str, ValueType] or an integer.",
|
1252
1331
|
)
|
1253
1332
|
if isinstance(market, tuple):
|
1254
1333
|
x_value = log(
|
1255
|
-
self.tsdf.loc[:, market] / self.tsdf.loc[:, market].iloc[0]
|
1334
|
+
self.tsdf.loc[:, market] / self.tsdf.loc[:, market].iloc[0],
|
1256
1335
|
)
|
1257
1336
|
elif isinstance(market, int):
|
1258
1337
|
x_value = log(self.tsdf.iloc[:, market] / self.tsdf.iloc[0, market])
|
1259
1338
|
else:
|
1260
1339
|
raise ValueError(
|
1261
|
-
"market should be a tuple[str, ValueType] or an integer."
|
1340
|
+
"market should be a tuple[str, ValueType] or an integer.",
|
1262
1341
|
)
|
1263
1342
|
|
1264
1343
|
covariance = cov(y_value, x_value, ddof=1)
|
@@ -1274,9 +1353,12 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1274
1353
|
method: LiteralOlsFitMethod = "pinv",
|
1275
1354
|
cov_type: LiteralOlsFitCovType = "nonrobust",
|
1276
1355
|
) -> RegressionResults:
|
1277
|
-
"""
|
1356
|
+
"""
|
1357
|
+
Ordinary Least Squares fit.
|
1358
|
+
|
1278
1359
|
Performs a linear regression and adds a new column with a fitted line
|
1279
1360
|
using Ordinary Least Squares fit
|
1361
|
+
https://www.statsmodels.org/stable/examples/notebooks/generated/ols.html.
|
1280
1362
|
|
1281
1363
|
Parameters
|
1282
1364
|
----------
|
@@ -1296,7 +1378,6 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1296
1378
|
RegressionResults
|
1297
1379
|
The Statsmodels regression output
|
1298
1380
|
"""
|
1299
|
-
|
1300
1381
|
if isinstance(y_column, tuple):
|
1301
1382
|
y_value = self.tsdf.loc[:, y_column]
|
1302
1383
|
y_label = self.tsdf.loc[:, y_column].name[0]
|
@@ -1305,7 +1386,7 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1305
1386
|
y_label = self.tsdf.iloc[:, y_column].name[0]
|
1306
1387
|
else:
|
1307
1388
|
raise ValueError(
|
1308
|
-
"y_column should be a tuple[str, ValueType] or an integer."
|
1389
|
+
"y_column should be a tuple[str, ValueType] or an integer.",
|
1309
1390
|
)
|
1310
1391
|
|
1311
1392
|
if isinstance(x_column, tuple):
|
@@ -1316,7 +1397,7 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1316
1397
|
x_label = self.tsdf.iloc[:, x_column].name[0]
|
1317
1398
|
else:
|
1318
1399
|
raise ValueError(
|
1319
|
-
"x_column should be a tuple[str, ValueType] or an integer."
|
1400
|
+
"x_column should be a tuple[str, ValueType] or an integer.",
|
1320
1401
|
)
|
1321
1402
|
|
1322
1403
|
results = sm.OLS(y_value, x_value).fit(method=method, cov_type=cov_type)
|
@@ -1339,7 +1420,8 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1339
1420
|
covar_method: LiteralCovMethod = "ledoit-wolf",
|
1340
1421
|
options: Optional[dict[str, int]] = None,
|
1341
1422
|
) -> DataFrame:
|
1342
|
-
"""
|
1423
|
+
"""
|
1424
|
+
Calculate a basket timeseries based on the supplied weights.
|
1343
1425
|
|
1344
1426
|
Parameters
|
1345
1427
|
----------
|
@@ -1374,11 +1456,12 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1374
1456
|
if self.weights is None and weight_strat is None:
|
1375
1457
|
raise ValueError(
|
1376
1458
|
"OpenFrame weights property must be provided to run the "
|
1377
|
-
"make_portfolio method."
|
1459
|
+
"make_portfolio method.",
|
1378
1460
|
)
|
1379
1461
|
dframe = self.tsdf.copy()
|
1380
1462
|
if not any(
|
1381
|
-
x == ValueType.RTRN
|
1463
|
+
x == ValueType.RTRN
|
1464
|
+
for x in self.tsdf.columns.get_level_values(1).to_numpy()
|
1382
1465
|
):
|
1383
1466
|
dframe = dframe.pct_change()
|
1384
1467
|
dframe.iloc[0] = 0
|
@@ -1427,7 +1510,10 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1427
1510
|
observations: int = 21,
|
1428
1511
|
periods_in_a_year_fixed: Optional[int] = None,
|
1429
1512
|
) -> DataFrame:
|
1430
|
-
"""
|
1513
|
+
"""
|
1514
|
+
Calculate rolling Information Ratio.
|
1515
|
+
|
1516
|
+
The Information Ratio equals ( fund return less index return ) divided by
|
1431
1517
|
the Tracking Error. And the Tracking Error is the standard deviation of the
|
1432
1518
|
difference between the fund and its index returns.
|
1433
1519
|
|
@@ -1447,7 +1533,6 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1447
1533
|
Pandas.DataFrame
|
1448
1534
|
Rolling Information Ratios
|
1449
1535
|
"""
|
1450
|
-
|
1451
1536
|
ratio_label = (
|
1452
1537
|
f"{self.tsdf.iloc[:, long_column].name[0]}"
|
1453
1538
|
f" / {self.tsdf.iloc[:, short_column].name[0]}"
|
@@ -1467,7 +1552,8 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1467
1552
|
retdf = retdf.dropna().to_frame()
|
1468
1553
|
|
1469
1554
|
voldf = relative.pct_change().rolling(
|
1470
|
-
observations,
|
1555
|
+
observations,
|
1556
|
+
min_periods=observations,
|
1471
1557
|
).std() * sqrt(time_factor)
|
1472
1558
|
voldf = voldf.dropna().to_frame()
|
1473
1559
|
|
@@ -1482,8 +1568,11 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1482
1568
|
market_column: int = 1,
|
1483
1569
|
observations: int = 21,
|
1484
1570
|
) -> DataFrame:
|
1485
|
-
"""
|
1486
|
-
|
1571
|
+
"""
|
1572
|
+
Calculate rolling Market Beta.
|
1573
|
+
|
1574
|
+
Calculates Beta as Co-variance of asset & market divided by Variance
|
1575
|
+
of the market. https://www.investopedia.com/terms/b/beta.asp.
|
1487
1576
|
|
1488
1577
|
Parameters
|
1489
1578
|
----------
|
@@ -1506,10 +1595,11 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1506
1595
|
rolling = rolling.pct_change().rolling(observations, min_periods=observations)
|
1507
1596
|
|
1508
1597
|
rcov = rolling.cov()
|
1509
|
-
rcov.dropna(
|
1598
|
+
rcov = rcov.dropna()
|
1510
1599
|
|
1511
1600
|
rollbeta = rcov.iloc[:, asset_column].xs(market_label, level=1) / rcov.iloc[
|
1512
|
-
:,
|
1601
|
+
:,
|
1602
|
+
market_column,
|
1513
1603
|
].xs(market_label, level=1)
|
1514
1604
|
rollbeta = rollbeta.to_frame()
|
1515
1605
|
rollbeta.index = rollbeta.index.droplevel(level=1)
|
@@ -1523,7 +1613,10 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1523
1613
|
second_column: int = 1,
|
1524
1614
|
observations: int = 21,
|
1525
1615
|
) -> DataFrame:
|
1526
|
-
"""
|
1616
|
+
"""
|
1617
|
+
Calculate rolling Correlation.
|
1618
|
+
|
1619
|
+
Calculates correlation between two series. The period with
|
1527
1620
|
at least the given number of observations is the first period calculated.
|
1528
1621
|
|
1529
1622
|
Parameters
|
@@ -1540,7 +1633,6 @@ class OpenFrame(BaseModel, CommonModel):
|
|
1540
1633
|
Pandas.DataFrame
|
1541
1634
|
Rolling Correlations
|
1542
1635
|
"""
|
1543
|
-
|
1544
1636
|
corr_label = (
|
1545
1637
|
self.tsdf.iloc[:, first_column].name[0]
|
1546
1638
|
+ "_VS_"
|