openseries 1.7.1__tar.gz → 1.7.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openseries-1.7.1 → openseries-1.7.3}/PKG-INFO +1 -2
- {openseries-1.7.1 → openseries-1.7.3}/README.md +0 -1
- {openseries-1.7.1 → openseries-1.7.3}/openseries/_common_model.py +38 -58
- {openseries-1.7.1 → openseries-1.7.3}/openseries/datefixer.py +7 -14
- {openseries-1.7.1 → openseries-1.7.3}/openseries/frame.py +65 -102
- {openseries-1.7.1 → openseries-1.7.3}/openseries/portfoliotools.py +2 -7
- {openseries-1.7.1 → openseries-1.7.3}/openseries/series.py +8 -10
- {openseries-1.7.1 → openseries-1.7.3}/openseries/simulation.py +4 -8
- {openseries-1.7.1 → openseries-1.7.3}/openseries/types.py +35 -55
- {openseries-1.7.1 → openseries-1.7.3}/pyproject.toml +4 -4
- {openseries-1.7.1 → openseries-1.7.3}/LICENSE.md +0 -0
- {openseries-1.7.1 → openseries-1.7.3}/openseries/__init__.py +0 -0
- {openseries-1.7.1 → openseries-1.7.3}/openseries/_risk.py +0 -0
- {openseries-1.7.1 → openseries-1.7.3}/openseries/load_plotly.py +0 -0
- {openseries-1.7.1 → openseries-1.7.3}/openseries/plotly_captor_logo.json +0 -0
- {openseries-1.7.1 → openseries-1.7.3}/openseries/plotly_layouts.json +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: openseries
|
3
|
-
Version: 1.7.
|
3
|
+
Version: 1.7.3
|
4
4
|
Summary: Tools for analyzing financial timeseries.
|
5
5
|
Home-page: https://github.com/CaptorAB/openseries
|
6
6
|
License: BSD-3-Clause
|
@@ -261,7 +261,6 @@ make lint
|
|
261
261
|
|
262
262
|
| Method | Applies to | Description |
|
263
263
|
|:-------------------------|:-----------------|:-----------------------------------------------------------------------------------------------------------------------------------------------|
|
264
|
-
| `setup_class` | `OpenTimeSeries` | Class method that defines the `domestic` home currency and the `countries` home countries attributes. |
|
265
264
|
| `pandas_df` | `OpenTimeSeries` | Method to create the `tsdf` pandas.DataFrame from the `dates` and `values`. |
|
266
265
|
| `set_new_label` | `OpenTimeSeries` | Method to change the pandas.DataFrame column MultiIndex. |
|
267
266
|
| `running_adjustment` | `OpenTimeSeries` | Adjusts the series performance with a `float` factor. |
|
@@ -226,7 +226,6 @@ make lint
|
|
226
226
|
|
227
227
|
| Method | Applies to | Description |
|
228
228
|
|:-------------------------|:-----------------|:-----------------------------------------------------------------------------------------------------------------------------------------------|
|
229
|
-
| `setup_class` | `OpenTimeSeries` | Class method that defines the `domestic` home currency and the `countries` home countries attributes. |
|
230
229
|
| `pandas_df` | `OpenTimeSeries` | Method to create the `tsdf` pandas.DataFrame from the `dates` and `values`. |
|
231
230
|
| `set_new_label` | `OpenTimeSeries` | Method to change the pandas.DataFrame column MultiIndex. |
|
232
231
|
| `running_adjustment` | `OpenTimeSeries` | Adjusts the series performance with a `float` factor. |
|
@@ -18,7 +18,15 @@ if TYPE_CHECKING:
|
|
18
18
|
from openpyxl.utils.dataframe import dataframe_to_rows
|
19
19
|
from openpyxl.workbook.workbook import Workbook
|
20
20
|
from openpyxl.worksheet.worksheet import Worksheet
|
21
|
-
from pandas import
|
21
|
+
from pandas import (
|
22
|
+
DataFrame,
|
23
|
+
DatetimeIndex,
|
24
|
+
Index,
|
25
|
+
MultiIndex,
|
26
|
+
Series,
|
27
|
+
date_range,
|
28
|
+
to_datetime,
|
29
|
+
)
|
22
30
|
from pandas.tseries.offsets import CustomBusinessDay
|
23
31
|
from plotly.graph_objs import Figure # type: ignore[import-untyped,unused-ignore]
|
24
32
|
from plotly.io import to_html # type: ignore[import-untyped,unused-ignore]
|
@@ -351,9 +359,7 @@ class _CommonModel(BaseModel):
|
|
351
359
|
"""
|
352
360
|
wmdf = self.tsdf.copy()
|
353
361
|
wmdf.index = DatetimeIndex(wmdf.index)
|
354
|
-
result = (
|
355
|
-
wmdf.resample("BME").last().pct_change(fill_method=cast(str, None)).min()
|
356
|
-
)
|
362
|
+
result = wmdf.resample("BME").last().pct_change().min()
|
357
363
|
|
358
364
|
if self.tsdf.shape[1] == 1:
|
359
365
|
return float(result.iloc[0])
|
@@ -484,9 +490,7 @@ class _CommonModel(BaseModel):
|
|
484
490
|
"Argument months_offset implies start"
|
485
491
|
"date before first date in series."
|
486
492
|
)
|
487
|
-
raise ValueError(
|
488
|
-
msg,
|
489
|
-
)
|
493
|
+
raise ValueError(msg)
|
490
494
|
later = self.last_idx
|
491
495
|
else:
|
492
496
|
if from_dt is not None:
|
@@ -523,8 +527,8 @@ class _CommonModel(BaseModel):
|
|
523
527
|
An OpenFrame object
|
524
528
|
|
525
529
|
"""
|
526
|
-
startyear =
|
527
|
-
endyear =
|
530
|
+
startyear = to_datetime(self.tsdf.index[0]).year
|
531
|
+
endyear = to_datetime(self.tsdf.index[-1]).year
|
528
532
|
calendar = holiday_calendar(
|
529
533
|
startyear=startyear,
|
530
534
|
endyear=endyear,
|
@@ -785,9 +789,7 @@ class _CommonModel(BaseModel):
|
|
785
789
|
if labels:
|
786
790
|
if len(labels) != self.tsdf.shape[1]:
|
787
791
|
msg = "Must provide same number of labels as items in frame."
|
788
|
-
raise ValueError(
|
789
|
-
msg,
|
790
|
-
)
|
792
|
+
raise ValueError(msg)
|
791
793
|
else:
|
792
794
|
labels = list(self.tsdf.columns.get_level_values(0))
|
793
795
|
|
@@ -902,9 +904,7 @@ class _CommonModel(BaseModel):
|
|
902
904
|
if labels:
|
903
905
|
if len(labels) != self.tsdf.shape[1]:
|
904
906
|
msg = "Must provide same number of labels as items in frame."
|
905
|
-
raise ValueError(
|
906
|
-
msg,
|
907
|
-
)
|
907
|
+
raise ValueError(msg)
|
908
908
|
else:
|
909
909
|
labels = list(self.tsdf.columns.get_level_values(0))
|
910
910
|
|
@@ -1027,9 +1027,7 @@ class _CommonModel(BaseModel):
|
|
1027
1027
|
time_factor = how_many / fraction
|
1028
1028
|
|
1029
1029
|
result = (
|
1030
|
-
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1031
|
-
.pct_change(fill_method=cast(str, None))
|
1032
|
-
.mean()
|
1030
|
+
self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change().mean()
|
1033
1031
|
* time_factor
|
1034
1032
|
)
|
1035
1033
|
|
@@ -1087,9 +1085,7 @@ class _CommonModel(BaseModel):
|
|
1087
1085
|
time_factor = how_many / fraction
|
1088
1086
|
|
1089
1087
|
data = self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1090
|
-
result = (
|
1091
|
-
data.pct_change(fill_method=cast(str, None)).std().mul(sqrt(time_factor))
|
1092
|
-
)
|
1088
|
+
result = data.pct_change().std().mul(sqrt(time_factor))
|
1093
1089
|
|
1094
1090
|
if self.tsdf.shape[1] == 1:
|
1095
1091
|
return float(cast(SupportsFloat, result.iloc[0]))
|
@@ -1285,22 +1281,20 @@ class _CommonModel(BaseModel):
|
|
1285
1281
|
if drift_adjust:
|
1286
1282
|
imp_vol = (-sqrt(time_factor) / norm.ppf(level)) * (
|
1287
1283
|
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1288
|
-
.pct_change(
|
1284
|
+
.pct_change()
|
1289
1285
|
.quantile(1 - level, interpolation=interpolation)
|
1290
1286
|
- self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1291
|
-
.pct_change(
|
1287
|
+
.pct_change()
|
1292
1288
|
.sum()
|
1293
1289
|
/ len(
|
1294
|
-
self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(
|
1295
|
-
fill_method=cast(str, None),
|
1296
|
-
),
|
1290
|
+
self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(),
|
1297
1291
|
)
|
1298
1292
|
)
|
1299
1293
|
else:
|
1300
1294
|
imp_vol = (
|
1301
1295
|
-sqrt(time_factor)
|
1302
1296
|
* self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1303
|
-
.pct_change(
|
1297
|
+
.pct_change()
|
1304
1298
|
.quantile(1 - level, interpolation=interpolation)
|
1305
1299
|
/ norm.ppf(level)
|
1306
1300
|
)
|
@@ -1363,14 +1357,14 @@ class _CommonModel(BaseModel):
|
|
1363
1357
|
cvar_df = self.tsdf.loc[cast(int, earlier) : cast(int, later)].copy(deep=True)
|
1364
1358
|
result = [
|
1365
1359
|
cvar_df.loc[:, x] # type: ignore[call-overload,index]
|
1366
|
-
.pct_change(
|
1360
|
+
.pct_change()
|
1367
1361
|
.sort_values()
|
1368
1362
|
.iloc[
|
1369
1363
|
: int(
|
1370
1364
|
ceil(
|
1371
1365
|
(1 - level)
|
1372
1366
|
* cvar_df.loc[:, x] # type: ignore[index]
|
1373
|
-
.pct_change(
|
1367
|
+
.pct_change()
|
1374
1368
|
.count(),
|
1375
1369
|
),
|
1376
1370
|
)
|
@@ -1430,7 +1424,7 @@ class _CommonModel(BaseModel):
|
|
1430
1424
|
)
|
1431
1425
|
how_many = (
|
1432
1426
|
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1433
|
-
.pct_change(
|
1427
|
+
.pct_change()
|
1434
1428
|
.count(numeric_only=True)
|
1435
1429
|
)
|
1436
1430
|
if periods_in_a_year_fixed:
|
@@ -1445,7 +1439,7 @@ class _CommonModel(BaseModel):
|
|
1445
1439
|
|
1446
1440
|
dddf = (
|
1447
1441
|
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1448
|
-
.pct_change(
|
1442
|
+
.pct_change()
|
1449
1443
|
.sub(min_accepted_return / time_factor)
|
1450
1444
|
)
|
1451
1445
|
|
@@ -1548,7 +1542,7 @@ class _CommonModel(BaseModel):
|
|
1548
1542
|
)
|
1549
1543
|
result: NDArray[float64] = skew(
|
1550
1544
|
a=self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1551
|
-
.pct_change(
|
1545
|
+
.pct_change()
|
1552
1546
|
.to_numpy(),
|
1553
1547
|
bias=True,
|
1554
1548
|
nan_policy="omit",
|
@@ -1595,9 +1589,7 @@ class _CommonModel(BaseModel):
|
|
1595
1589
|
to_dt=to_date,
|
1596
1590
|
)
|
1597
1591
|
result: NDArray[float64] = kurtosis(
|
1598
|
-
self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(
|
1599
|
-
fill_method=cast(str, None),
|
1600
|
-
),
|
1592
|
+
self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(),
|
1601
1593
|
fisher=True,
|
1602
1594
|
bias=True,
|
1603
1595
|
nan_policy="omit",
|
@@ -1693,19 +1685,13 @@ class _CommonModel(BaseModel):
|
|
1693
1685
|
)
|
1694
1686
|
pos = (
|
1695
1687
|
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1696
|
-
.pct_change(
|
1697
|
-
self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(
|
1698
|
-
fill_method=cast(str, None),
|
1699
|
-
)[1:]
|
1688
|
+
.pct_change()[1:][
|
1689
|
+
self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change()[1:]
|
1700
1690
|
> zero
|
1701
1691
|
]
|
1702
1692
|
.count()
|
1703
1693
|
)
|
1704
|
-
tot = (
|
1705
|
-
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
1706
|
-
.pct_change(fill_method=cast(str, None))[1:]
|
1707
|
-
.count()
|
1708
|
-
)
|
1694
|
+
tot = self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change().count()
|
1709
1695
|
share = pos / tot
|
1710
1696
|
if self.tsdf.shape[1] == 1:
|
1711
1697
|
return float(share.iloc[0])
|
@@ -1881,9 +1867,7 @@ class _CommonModel(BaseModel):
|
|
1881
1867
|
from_dt=from_date,
|
1882
1868
|
to_dt=to_date,
|
1883
1869
|
)
|
1884
|
-
retdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(
|
1885
|
-
fill_method=cast(str, None),
|
1886
|
-
)
|
1870
|
+
retdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change()
|
1887
1871
|
pos = retdf[retdf > min_accepted_return].sub(min_accepted_return).sum()
|
1888
1872
|
neg = retdf[retdf < min_accepted_return].sub(min_accepted_return).sum()
|
1889
1873
|
ratio = pos / -neg
|
@@ -1932,9 +1916,7 @@ class _CommonModel(BaseModel):
|
|
1932
1916
|
"Simple return cannot be calculated due to "
|
1933
1917
|
f"an initial value being zero. ({self.tsdf.head(3)})"
|
1934
1918
|
)
|
1935
|
-
raise ValueError(
|
1936
|
-
msg,
|
1937
|
-
)
|
1919
|
+
raise ValueError(msg)
|
1938
1920
|
|
1939
1921
|
result = self.tsdf.loc[later] / self.tsdf.loc[earlier] - 1
|
1940
1922
|
|
@@ -1973,7 +1955,7 @@ class _CommonModel(BaseModel):
|
|
1973
1955
|
period = "-".join([str(year), str(month).zfill(2)])
|
1974
1956
|
vrdf = self.tsdf.copy()
|
1975
1957
|
vrdf.index = DatetimeIndex(vrdf.index)
|
1976
|
-
resultdf = DataFrame(vrdf.pct_change(
|
1958
|
+
resultdf = DataFrame(vrdf.pct_change())
|
1977
1959
|
result = resultdf.loc[period] + 1
|
1978
1960
|
cal_period = result.cumprod(axis="index").iloc[-1] - 1
|
1979
1961
|
if self.tsdf.shape[1] == 1:
|
@@ -2025,7 +2007,7 @@ class _CommonModel(BaseModel):
|
|
2025
2007
|
)
|
2026
2008
|
result = (
|
2027
2009
|
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
2028
|
-
.pct_change(
|
2010
|
+
.pct_change()
|
2029
2011
|
.quantile(1 - level, interpolation=interpolation)
|
2030
2012
|
)
|
2031
2013
|
|
@@ -2073,7 +2055,7 @@ class _CommonModel(BaseModel):
|
|
2073
2055
|
)
|
2074
2056
|
result = (
|
2075
2057
|
self.tsdf.loc[cast(int, earlier) : cast(int, later)]
|
2076
|
-
.pct_change(
|
2058
|
+
.pct_change()
|
2077
2059
|
.rolling(observations, min_periods=observations)
|
2078
2060
|
.sum()
|
2079
2061
|
.min()
|
@@ -2119,9 +2101,7 @@ class _CommonModel(BaseModel):
|
|
2119
2101
|
from_dt=from_date,
|
2120
2102
|
to_dt=to_date,
|
2121
2103
|
)
|
2122
|
-
zscframe = self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change(
|
2123
|
-
fill_method=cast(str, None),
|
2124
|
-
)
|
2104
|
+
zscframe = self.tsdf.loc[cast(int, earlier) : cast(int, later)].pct_change()
|
2125
2105
|
result = (zscframe.iloc[-1] - zscframe.mean()) / zscframe.std()
|
2126
2106
|
|
2127
2107
|
if self.tsdf.shape[1] == 1:
|
@@ -2190,7 +2170,7 @@ class _CommonModel(BaseModel):
|
|
2190
2170
|
ret_label = cast(tuple[str], self.tsdf.iloc[:, column].name)[0]
|
2191
2171
|
retseries = (
|
2192
2172
|
self.tsdf.iloc[:, column]
|
2193
|
-
.pct_change(
|
2173
|
+
.pct_change()
|
2194
2174
|
.rolling(observations, min_periods=observations)
|
2195
2175
|
.sum()
|
2196
2176
|
)
|
@@ -2267,7 +2247,7 @@ class _CommonModel(BaseModel):
|
|
2267
2247
|
else:
|
2268
2248
|
time_factor = self.periods_in_a_year
|
2269
2249
|
vol_label = cast(tuple[str, ValueType], self.tsdf.iloc[:, column].name)[0]
|
2270
|
-
dframe = self.tsdf.iloc[:, column].pct_change(
|
2250
|
+
dframe = self.tsdf.iloc[:, column].pct_change()
|
2271
2251
|
volseries = dframe.rolling(
|
2272
2252
|
observations,
|
2273
2253
|
min_periods=observations,
|
@@ -92,9 +92,7 @@ def holiday_calendar(
|
|
92
92
|
"Argument countries must be a string country code or "
|
93
93
|
"a list of string country codes according to ISO 3166-1 alpha-2."
|
94
94
|
)
|
95
|
-
raise ValueError(
|
96
|
-
msg,
|
97
|
-
)
|
95
|
+
raise ValueError(msg)
|
98
96
|
|
99
97
|
return busdaycalendar(holidays=hols)
|
100
98
|
|
@@ -115,6 +113,7 @@ def date_fix(
|
|
115
113
|
Parsed date
|
116
114
|
|
117
115
|
"""
|
116
|
+
msg = f"Unknown date format {fixerdate!s} of type {type(fixerdate)!s} encountered"
|
118
117
|
if isinstance(fixerdate, (Timestamp, dt.datetime)):
|
119
118
|
return fixerdate.date()
|
120
119
|
if isinstance(fixerdate, dt.date):
|
@@ -125,10 +124,7 @@ def date_fix(
|
|
125
124
|
)
|
126
125
|
if isinstance(fixerdate, str):
|
127
126
|
return dt.datetime.strptime(fixerdate, "%Y-%m-%d").astimezone().date()
|
128
|
-
|
129
|
-
raise TypeError(
|
130
|
-
msg,
|
131
|
-
)
|
127
|
+
raise TypeError(msg)
|
132
128
|
|
133
129
|
|
134
130
|
def date_offset_foll(
|
@@ -340,7 +336,7 @@ def generate_calendar_date_range(
|
|
340
336
|
)
|
341
337
|
calendar = holiday_calendar(
|
342
338
|
startyear=start.year,
|
343
|
-
endyear=tmp_range[-1].year,
|
339
|
+
endyear=date_fix(tmp_range[-1]).year,
|
344
340
|
countries=countries,
|
345
341
|
)
|
346
342
|
return [
|
@@ -355,7 +351,7 @@ def generate_calendar_date_range(
|
|
355
351
|
if end and not start:
|
356
352
|
tmp_range = date_range(end=end, periods=trading_days * 365 // 252, freq="D")
|
357
353
|
calendar = holiday_calendar(
|
358
|
-
startyear=tmp_range[0].year,
|
354
|
+
startyear=date_fix(tmp_range[0]).year,
|
359
355
|
endyear=end.year,
|
360
356
|
countries=countries,
|
361
357
|
)
|
@@ -372,12 +368,9 @@ def generate_calendar_date_range(
|
|
372
368
|
"Provide one of start or end date, but not both. "
|
373
369
|
"Date range is inferred from number of trading days."
|
374
370
|
)
|
375
|
-
raise ValueError(
|
376
|
-
msg,
|
377
|
-
)
|
371
|
+
raise ValueError(msg)
|
378
372
|
|
379
373
|
|
380
|
-
# noinspection PyUnusedLocal
|
381
374
|
def _do_resample_to_business_period_ends(
|
382
375
|
data: DataFrame,
|
383
376
|
freq: LiteralBizDayFreq,
|
@@ -427,4 +420,4 @@ def _do_resample_to_business_period_ends(
|
|
427
420
|
]
|
428
421
|
+ [copydata.index[-1]],
|
429
422
|
)
|
430
|
-
return dates.drop_duplicates()
|
423
|
+
return DatetimeIndex(dates.drop_duplicates())
|
@@ -13,7 +13,6 @@ if TYPE_CHECKING:
|
|
13
13
|
|
14
14
|
import statsmodels.api as sm # type: ignore[import-untyped,unused-ignore]
|
15
15
|
from numpy import (
|
16
|
-
array,
|
17
16
|
cov,
|
18
17
|
cumprod,
|
19
18
|
divide,
|
@@ -337,7 +336,7 @@ class OpenFrame(_CommonModel):
|
|
337
336
|
The returns of the values in the series
|
338
337
|
|
339
338
|
"""
|
340
|
-
self.tsdf = self.tsdf.pct_change(
|
339
|
+
self.tsdf = self.tsdf.pct_change()
|
341
340
|
self.tsdf.iloc[0] = 0
|
342
341
|
new_labels = [ValueType.RTRN] * self.item_count
|
343
342
|
arrays = [self.tsdf.columns.get_level_values(0), new_labels]
|
@@ -450,7 +449,8 @@ class OpenFrame(_CommonModel):
|
|
450
449
|
countries=countries,
|
451
450
|
)
|
452
451
|
xerie.tsdf = xerie.tsdf.reindex(
|
453
|
-
[deyt.date() for deyt in dates],
|
452
|
+
[deyt.date() for deyt in dates],
|
453
|
+
method=method,
|
454
454
|
)
|
455
455
|
|
456
456
|
self._set_tsdf()
|
@@ -589,7 +589,7 @@ class OpenFrame(_CommonModel):
|
|
589
589
|
Correlation matrix
|
590
590
|
|
591
591
|
"""
|
592
|
-
corr_matrix = self.tsdf.pct_change(
|
592
|
+
corr_matrix = self.tsdf.pct_change().corr(
|
593
593
|
method="pearson",
|
594
594
|
min_periods=1,
|
595
595
|
)
|
@@ -616,7 +616,6 @@ class OpenFrame(_CommonModel):
|
|
616
616
|
|
617
617
|
"""
|
618
618
|
self.constituents += [new_series]
|
619
|
-
# noinspection PyUnreachableCode
|
620
619
|
self.tsdf = concat([self.tsdf, new_series.tsdf], axis="columns", sort=True)
|
621
620
|
return self
|
622
621
|
|
@@ -777,6 +776,7 @@ class OpenFrame(_CommonModel):
|
|
777
776
|
earlier, later = self.calc_range(months_from_last, from_date, to_date)
|
778
777
|
fraction = (later - earlier).days / 365.25
|
779
778
|
|
779
|
+
msg = "base_column should be a tuple[str, ValueType] or an integer."
|
780
780
|
if isinstance(base_column, tuple):
|
781
781
|
shortdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].loc[
|
782
782
|
:,
|
@@ -798,10 +798,7 @@ class OpenFrame(_CommonModel):
|
|
798
798
|
].name
|
799
799
|
short_label = cast(tuple[str, str], self.tsdf.iloc[:, base_column].name)[0]
|
800
800
|
else:
|
801
|
-
msg
|
802
|
-
raise TypeError(
|
803
|
-
msg,
|
804
|
-
)
|
801
|
+
raise TypeError(msg)
|
805
802
|
|
806
803
|
if periods_in_a_year_fixed:
|
807
804
|
time_factor = float(periods_in_a_year_fixed)
|
@@ -817,10 +814,9 @@ class OpenFrame(_CommonModel):
|
|
817
814
|
:,
|
818
815
|
item,
|
819
816
|
]
|
820
|
-
# noinspection PyTypeChecker
|
821
817
|
relative = 1.0 + longdf - shortdf
|
822
818
|
vol = float(
|
823
|
-
relative.pct_change(
|
819
|
+
relative.pct_change().std() * sqrt(time_factor),
|
824
820
|
)
|
825
821
|
terrors.append(vol)
|
826
822
|
|
@@ -870,6 +866,7 @@ class OpenFrame(_CommonModel):
|
|
870
866
|
earlier, later = self.calc_range(months_from_last, from_date, to_date)
|
871
867
|
fraction = (later - earlier).days / 365.25
|
872
868
|
|
869
|
+
msg = "base_column should be a tuple[str, ValueType] or an integer."
|
873
870
|
if isinstance(base_column, tuple):
|
874
871
|
shortdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].loc[
|
875
872
|
:,
|
@@ -891,10 +888,7 @@ class OpenFrame(_CommonModel):
|
|
891
888
|
].name
|
892
889
|
short_label = cast(tuple[str, str], self.tsdf.iloc[:, base_column].name)[0]
|
893
890
|
else:
|
894
|
-
msg
|
895
|
-
raise TypeError(
|
896
|
-
msg,
|
897
|
-
)
|
891
|
+
raise TypeError(msg)
|
898
892
|
|
899
893
|
if periods_in_a_year_fixed:
|
900
894
|
time_factor = float(periods_in_a_year_fixed)
|
@@ -910,13 +904,12 @@ class OpenFrame(_CommonModel):
|
|
910
904
|
:,
|
911
905
|
item,
|
912
906
|
]
|
913
|
-
# noinspection PyTypeChecker
|
914
907
|
relative = 1.0 + longdf - shortdf
|
915
908
|
ret = float(
|
916
|
-
relative.pct_change(
|
909
|
+
relative.pct_change().mean() * time_factor,
|
917
910
|
)
|
918
911
|
vol = float(
|
919
|
-
relative.pct_change(
|
912
|
+
relative.pct_change().std() * sqrt(time_factor),
|
920
913
|
)
|
921
914
|
ratios.append(ret / vol)
|
922
915
|
|
@@ -974,6 +967,7 @@ class OpenFrame(_CommonModel):
|
|
974
967
|
earlier, later = self.calc_range(months_from_last, from_date, to_date)
|
975
968
|
fraction = (later - earlier).days / 365.25
|
976
969
|
|
970
|
+
msg = "base_column should be a tuple[str, ValueType] or an integer."
|
977
971
|
if isinstance(base_column, tuple):
|
978
972
|
shortdf = self.tsdf.loc[cast(int, earlier) : cast(int, later)].loc[
|
979
973
|
:,
|
@@ -995,10 +989,7 @@ class OpenFrame(_CommonModel):
|
|
995
989
|
].name
|
996
990
|
short_label = cast(tuple[str, str], self.tsdf.iloc[:, base_column].name)[0]
|
997
991
|
else:
|
998
|
-
msg
|
999
|
-
raise TypeError(
|
1000
|
-
msg,
|
1001
|
-
)
|
992
|
+
raise TypeError(msg)
|
1002
993
|
|
1003
994
|
if periods_in_a_year_fixed:
|
1004
995
|
time_factor = float(periods_in_a_year_fixed)
|
@@ -1014,20 +1005,19 @@ class OpenFrame(_CommonModel):
|
|
1014
1005
|
:,
|
1015
1006
|
item,
|
1016
1007
|
]
|
1008
|
+
msg = "ratio must be one of 'up', 'down' or 'both'."
|
1017
1009
|
if ratio == "up":
|
1018
1010
|
uparray = (
|
1019
|
-
longdf.pct_change(
|
1020
|
-
shortdf.pct_change(
|
1021
|
-
> loss_limit
|
1011
|
+
longdf.pct_change()[
|
1012
|
+
shortdf.pct_change().to_numpy() > loss_limit
|
1022
1013
|
]
|
1023
1014
|
.add(1)
|
1024
1015
|
.to_numpy()
|
1025
1016
|
)
|
1026
1017
|
up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
|
1027
1018
|
upidxarray = (
|
1028
|
-
shortdf.pct_change(
|
1029
|
-
shortdf.pct_change(
|
1030
|
-
> loss_limit
|
1019
|
+
shortdf.pct_change()[
|
1020
|
+
shortdf.pct_change().to_numpy() > loss_limit
|
1031
1021
|
]
|
1032
1022
|
.add(1)
|
1033
1023
|
.to_numpy()
|
@@ -1038,9 +1028,8 @@ class OpenFrame(_CommonModel):
|
|
1038
1028
|
ratios.append(up_rtrn / up_idx_return)
|
1039
1029
|
elif ratio == "down":
|
1040
1030
|
downarray = (
|
1041
|
-
longdf.pct_change(
|
1042
|
-
shortdf.pct_change(
|
1043
|
-
< loss_limit
|
1031
|
+
longdf.pct_change()[
|
1032
|
+
shortdf.pct_change().to_numpy() < loss_limit
|
1044
1033
|
]
|
1045
1034
|
.add(1)
|
1046
1035
|
.to_numpy()
|
@@ -1049,9 +1038,8 @@ class OpenFrame(_CommonModel):
|
|
1049
1038
|
downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
|
1050
1039
|
)
|
1051
1040
|
downidxarray = (
|
1052
|
-
shortdf.pct_change(
|
1053
|
-
shortdf.pct_change(
|
1054
|
-
< loss_limit
|
1041
|
+
shortdf.pct_change()[
|
1042
|
+
shortdf.pct_change().to_numpy() < loss_limit
|
1055
1043
|
]
|
1056
1044
|
.add(1)
|
1057
1045
|
.to_numpy()
|
@@ -1063,18 +1051,16 @@ class OpenFrame(_CommonModel):
|
|
1063
1051
|
ratios.append(down_return / down_idx_return)
|
1064
1052
|
elif ratio == "both":
|
1065
1053
|
uparray = (
|
1066
|
-
longdf.pct_change(
|
1067
|
-
shortdf.pct_change(
|
1068
|
-
> loss_limit
|
1054
|
+
longdf.pct_change()[
|
1055
|
+
shortdf.pct_change().to_numpy() > loss_limit
|
1069
1056
|
]
|
1070
1057
|
.add(1)
|
1071
1058
|
.to_numpy()
|
1072
1059
|
)
|
1073
1060
|
up_rtrn = uparray.prod() ** (1 / (len(uparray) / time_factor)) - 1
|
1074
1061
|
upidxarray = (
|
1075
|
-
shortdf.pct_change(
|
1076
|
-
shortdf.pct_change(
|
1077
|
-
> loss_limit
|
1062
|
+
shortdf.pct_change()[
|
1063
|
+
shortdf.pct_change().to_numpy() > loss_limit
|
1078
1064
|
]
|
1079
1065
|
.add(1)
|
1080
1066
|
.to_numpy()
|
@@ -1083,9 +1069,8 @@ class OpenFrame(_CommonModel):
|
|
1083
1069
|
upidxarray.prod() ** (1 / (len(upidxarray) / time_factor)) - 1
|
1084
1070
|
)
|
1085
1071
|
downarray = (
|
1086
|
-
longdf.pct_change(
|
1087
|
-
shortdf.pct_change(
|
1088
|
-
< loss_limit
|
1072
|
+
longdf.pct_change()[
|
1073
|
+
shortdf.pct_change().to_numpy() < loss_limit
|
1089
1074
|
]
|
1090
1075
|
.add(1)
|
1091
1076
|
.to_numpy()
|
@@ -1094,9 +1079,8 @@ class OpenFrame(_CommonModel):
|
|
1094
1079
|
downarray.prod() ** (1 / (len(downarray) / time_factor)) - 1
|
1095
1080
|
)
|
1096
1081
|
downidxarray = (
|
1097
|
-
shortdf.pct_change(
|
1098
|
-
shortdf.pct_change(
|
1099
|
-
< loss_limit
|
1082
|
+
shortdf.pct_change()[
|
1083
|
+
shortdf.pct_change().to_numpy() < loss_limit
|
1100
1084
|
]
|
1101
1085
|
.add(1)
|
1102
1086
|
.to_numpy()
|
@@ -1109,7 +1093,6 @@ class OpenFrame(_CommonModel):
|
|
1109
1093
|
(up_rtrn / up_idx_return) / (down_return / down_idx_return),
|
1110
1094
|
)
|
1111
1095
|
else:
|
1112
|
-
msg = "ratio must be one of 'up', 'down' or 'both'."
|
1113
1096
|
raise ValueError(msg)
|
1114
1097
|
|
1115
1098
|
if ratio == "up":
|
@@ -1156,25 +1139,23 @@ class OpenFrame(_CommonModel):
|
|
1156
1139
|
x_value == ValueType.RTRN
|
1157
1140
|
for x_value in self.tsdf.columns.get_level_values(1).to_numpy()
|
1158
1141
|
):
|
1142
|
+
msg = "asset should be a tuple[str, ValueType] or an integer."
|
1159
1143
|
if isinstance(asset, tuple):
|
1160
1144
|
y_value = self.tsdf.loc[:, asset]
|
1161
1145
|
elif isinstance(asset, int):
|
1162
1146
|
y_value = self.tsdf.iloc[:, asset]
|
1163
1147
|
else:
|
1164
|
-
msg
|
1165
|
-
|
1166
|
-
|
1167
|
-
)
|
1148
|
+
raise TypeError(msg)
|
1149
|
+
|
1150
|
+
msg = "market should be a tuple[str, ValueType] or an integer."
|
1168
1151
|
if isinstance(market, tuple):
|
1169
1152
|
x_value = self.tsdf.loc[:, market]
|
1170
1153
|
elif isinstance(market, int):
|
1171
1154
|
x_value = self.tsdf.iloc[:, market]
|
1172
1155
|
else:
|
1173
|
-
msg
|
1174
|
-
raise TypeError(
|
1175
|
-
msg,
|
1176
|
-
)
|
1156
|
+
raise TypeError(msg)
|
1177
1157
|
else:
|
1158
|
+
msg = "asset should be a tuple[str, ValueType] or an integer."
|
1178
1159
|
if isinstance(asset, tuple):
|
1179
1160
|
y_value = log(
|
1180
1161
|
self.tsdf.loc[:, asset] / self.tsdf.loc[:, asset].iloc[0],
|
@@ -1184,10 +1165,9 @@ class OpenFrame(_CommonModel):
|
|
1184
1165
|
self.tsdf.iloc[:, asset] / cast(float, self.tsdf.iloc[0, asset]),
|
1185
1166
|
)
|
1186
1167
|
else:
|
1187
|
-
msg
|
1188
|
-
|
1189
|
-
|
1190
|
-
)
|
1168
|
+
raise TypeError(msg)
|
1169
|
+
|
1170
|
+
msg = "market should be a tuple[str, ValueType] or an integer."
|
1191
1171
|
if isinstance(market, tuple):
|
1192
1172
|
x_value = log(
|
1193
1173
|
self.tsdf.loc[:, market] / self.tsdf.loc[:, market].iloc[0],
|
@@ -1197,10 +1177,7 @@ class OpenFrame(_CommonModel):
|
|
1197
1177
|
self.tsdf.iloc[:, market] / cast(float, self.tsdf.iloc[0, market]),
|
1198
1178
|
)
|
1199
1179
|
else:
|
1200
|
-
msg
|
1201
|
-
raise TypeError(
|
1202
|
-
msg,
|
1203
|
-
)
|
1180
|
+
raise TypeError(msg)
|
1204
1181
|
|
1205
1182
|
covariance = cov(y_value, x_value, ddof=dlta_degr_freedms)
|
1206
1183
|
beta = covariance[0, 1] / covariance[1, 1]
|
@@ -1241,6 +1218,7 @@ class OpenFrame(_CommonModel):
|
|
1241
1218
|
The Statsmodels regression output
|
1242
1219
|
|
1243
1220
|
"""
|
1221
|
+
msg = "y_column should be a tuple[str, ValueType] or an integer."
|
1244
1222
|
if isinstance(y_column, tuple):
|
1245
1223
|
y_value = self.tsdf.loc[:, y_column]
|
1246
1224
|
y_label = cast(
|
@@ -1251,11 +1229,9 @@ class OpenFrame(_CommonModel):
|
|
1251
1229
|
y_value = self.tsdf.iloc[:, y_column]
|
1252
1230
|
y_label = cast(tuple[str, str], self.tsdf.iloc[:, y_column].name)[0]
|
1253
1231
|
else:
|
1254
|
-
msg
|
1255
|
-
raise TypeError(
|
1256
|
-
msg,
|
1257
|
-
)
|
1232
|
+
raise TypeError(msg)
|
1258
1233
|
|
1234
|
+
msg = "x_column should be a tuple[str, ValueType] or an integer."
|
1259
1235
|
if isinstance(x_column, tuple):
|
1260
1236
|
x_value = self.tsdf.loc[:, x_column]
|
1261
1237
|
x_label = cast(
|
@@ -1266,10 +1242,7 @@ class OpenFrame(_CommonModel):
|
|
1266
1242
|
x_value = self.tsdf.iloc[:, x_column]
|
1267
1243
|
x_label = cast(tuple[str, str], self.tsdf.iloc[:, x_column].name)[0]
|
1268
1244
|
else:
|
1269
|
-
msg
|
1270
|
-
raise TypeError(
|
1271
|
-
msg,
|
1272
|
-
)
|
1245
|
+
raise TypeError(msg)
|
1273
1246
|
|
1274
1247
|
results = sm.OLS(y_value, x_value).fit(method=method, cov_type=cov_type)
|
1275
1248
|
if fitted_series:
|
@@ -1315,6 +1288,7 @@ class OpenFrame(_CommonModel):
|
|
1315
1288
|
x == ValueType.RTRN
|
1316
1289
|
for x in self.tsdf.columns.get_level_values(1).to_numpy()
|
1317
1290
|
):
|
1291
|
+
msg = "asset should be a tuple[str, ValueType] or an integer."
|
1318
1292
|
if isinstance(asset, tuple):
|
1319
1293
|
asset_log = self.tsdf.loc[:, asset]
|
1320
1294
|
asset_cagr = asset_log.mean()
|
@@ -1322,10 +1296,9 @@ class OpenFrame(_CommonModel):
|
|
1322
1296
|
asset_log = self.tsdf.iloc[:, asset]
|
1323
1297
|
asset_cagr = asset_log.mean()
|
1324
1298
|
else:
|
1325
|
-
msg
|
1326
|
-
|
1327
|
-
|
1328
|
-
)
|
1299
|
+
raise TypeError(msg)
|
1300
|
+
|
1301
|
+
msg = "market should be a tuple[str, ValueType] or an integer."
|
1329
1302
|
if isinstance(market, tuple):
|
1330
1303
|
market_log = self.tsdf.loc[:, market]
|
1331
1304
|
market_cagr = market_log.mean()
|
@@ -1333,11 +1306,9 @@ class OpenFrame(_CommonModel):
|
|
1333
1306
|
market_log = self.tsdf.iloc[:, market]
|
1334
1307
|
market_cagr = market_log.mean()
|
1335
1308
|
else:
|
1336
|
-
msg
|
1337
|
-
raise TypeError(
|
1338
|
-
msg,
|
1339
|
-
)
|
1309
|
+
raise TypeError(msg)
|
1340
1310
|
else:
|
1311
|
+
msg = "asset should be a tuple[str, ValueType] or an integer."
|
1341
1312
|
if isinstance(asset, tuple):
|
1342
1313
|
asset_log = log(
|
1343
1314
|
self.tsdf.loc[:, asset] / self.tsdf.loc[:, asset].iloc[0],
|
@@ -1369,10 +1340,9 @@ class OpenFrame(_CommonModel):
|
|
1369
1340
|
- 1
|
1370
1341
|
)
|
1371
1342
|
else:
|
1372
|
-
msg
|
1373
|
-
|
1374
|
-
|
1375
|
-
)
|
1343
|
+
raise TypeError(msg)
|
1344
|
+
|
1345
|
+
msg = "market should be a tuple[str, ValueType] or an integer."
|
1376
1346
|
if isinstance(market, tuple):
|
1377
1347
|
market_log = log(
|
1378
1348
|
self.tsdf.loc[:, market] / self.tsdf.loc[:, market].iloc[0],
|
@@ -1404,10 +1374,7 @@ class OpenFrame(_CommonModel):
|
|
1404
1374
|
- 1
|
1405
1375
|
)
|
1406
1376
|
else:
|
1407
|
-
msg
|
1408
|
-
raise TypeError(
|
1409
|
-
msg,
|
1410
|
-
)
|
1377
|
+
raise TypeError(msg)
|
1411
1378
|
|
1412
1379
|
covariance = cov(asset_log, market_log, ddof=dlta_degr_freedms)
|
1413
1380
|
beta = covariance[0, 1] / covariance[1, 1]
|
@@ -1439,16 +1406,16 @@ class OpenFrame(_CommonModel):
|
|
1439
1406
|
"OpenFrame weights property must be provided "
|
1440
1407
|
"to run the make_portfolio method."
|
1441
1408
|
)
|
1442
|
-
raise ValueError(
|
1443
|
-
msg,
|
1444
|
-
)
|
1409
|
+
raise ValueError(msg)
|
1445
1410
|
dframe = self.tsdf.copy()
|
1446
1411
|
if not any(
|
1447
1412
|
x == ValueType.RTRN
|
1448
1413
|
for x in self.tsdf.columns.get_level_values(1).to_numpy()
|
1449
1414
|
):
|
1450
|
-
dframe = dframe.pct_change(
|
1415
|
+
dframe = dframe.pct_change()
|
1451
1416
|
dframe.iloc[0] = 0
|
1417
|
+
|
1418
|
+
msg = "Weight strategy not implemented"
|
1452
1419
|
if weight_strat:
|
1453
1420
|
if weight_strat == "eq_weights":
|
1454
1421
|
self.weights = [1.0 / self.item_count] * self.item_count
|
@@ -1457,10 +1424,10 @@ class OpenFrame(_CommonModel):
|
|
1457
1424
|
vol[isinf(vol)] = nan
|
1458
1425
|
self.weights = list(divide(vol, vol.sum()))
|
1459
1426
|
else:
|
1460
|
-
msg = "Weight strategy not implemented"
|
1461
1427
|
raise NotImplementedError(msg)
|
1428
|
+
|
1462
1429
|
return DataFrame(
|
1463
|
-
data=dframe
|
1430
|
+
data=(dframe @ self.weights).add(1.0).cumprod(),
|
1464
1431
|
index=self.tsdf.index,
|
1465
1432
|
columns=[[name], [ValueType.PRICE]],
|
1466
1433
|
dtype="float64",
|
@@ -1515,13 +1482,11 @@ class OpenFrame(_CommonModel):
|
|
1515
1482
|
)
|
1516
1483
|
|
1517
1484
|
retseries = (
|
1518
|
-
relative.pct_change(
|
1519
|
-
.rolling(observations, min_periods=observations)
|
1520
|
-
.sum()
|
1485
|
+
relative.pct_change().rolling(observations, min_periods=observations).sum()
|
1521
1486
|
)
|
1522
1487
|
retdf = retseries.dropna().to_frame()
|
1523
1488
|
|
1524
|
-
voldf = relative.pct_change(
|
1489
|
+
voldf = relative.pct_change().rolling(
|
1525
1490
|
observations,
|
1526
1491
|
min_periods=observations,
|
1527
1492
|
).std() * sqrt(time_factor)
|
@@ -1565,7 +1530,7 @@ class OpenFrame(_CommonModel):
|
|
1565
1530
|
asset_label = cast(tuple[str, str], self.tsdf.iloc[:, asset_column].name)[0]
|
1566
1531
|
beta_label = f"{asset_label} / {market_label}"
|
1567
1532
|
|
1568
|
-
rolling = self.tsdf.pct_change(
|
1533
|
+
rolling = self.tsdf.pct_change().rolling(
|
1569
1534
|
observations,
|
1570
1535
|
min_periods=observations,
|
1571
1536
|
)
|
@@ -1622,12 +1587,10 @@ class OpenFrame(_CommonModel):
|
|
1622
1587
|
)
|
1623
1588
|
first_series = (
|
1624
1589
|
self.tsdf.iloc[:, first_column]
|
1625
|
-
.pct_change(
|
1590
|
+
.pct_change()[1:]
|
1626
1591
|
.rolling(observations, min_periods=observations)
|
1627
1592
|
)
|
1628
|
-
second_series = self.tsdf.iloc[:, second_column].pct_change(
|
1629
|
-
fill_method=None,
|
1630
|
-
)[1:]
|
1593
|
+
second_series = self.tsdf.iloc[:, second_column].pct_change()[1:]
|
1631
1594
|
corrdf = first_series.corr(other=second_series).dropna().to_frame()
|
1632
1595
|
corrdf.columns = MultiIndex.from_arrays(
|
1633
1596
|
[
|
@@ -10,7 +10,6 @@ from typing import TYPE_CHECKING, Callable, cast
|
|
10
10
|
from numpy import (
|
11
11
|
append,
|
12
12
|
array,
|
13
|
-
dot,
|
14
13
|
float64,
|
15
14
|
inf,
|
16
15
|
isnan,
|
@@ -107,17 +106,13 @@ def simulate_portfolios(
|
|
107
106
|
all_weights[x, :] = weights
|
108
107
|
|
109
108
|
vol_arr[x] = sqrt(
|
110
|
-
|
111
|
-
weights.T,
|
112
|
-
dot(log_ret.cov() * simframe.periods_in_a_year, weights),
|
113
|
-
),
|
109
|
+
weights.T @ (log_ret.cov() * simframe.periods_in_a_year @ weights),
|
114
110
|
)
|
115
111
|
|
116
112
|
ret_arr[x] = npsum(log_ret.mean() * weights * simframe.periods_in_a_year)
|
117
113
|
|
118
114
|
sharpe_arr[x] = ret_arr[x] / vol_arr[x]
|
119
115
|
|
120
|
-
# noinspection PyUnreachableCode
|
121
116
|
simdf = concat(
|
122
117
|
[
|
123
118
|
DataFrame({"stdev": vol_arr, "ret": ret_arr, "sharpe": sharpe_arr}),
|
@@ -198,7 +193,7 @@ def efficient_frontier( # noqa: C901
|
|
198
193
|
per_in_yr: float,
|
199
194
|
) -> NDArray[float64]:
|
200
195
|
ret = npsum(lg_ret.mean() * weights) * per_in_yr
|
201
|
-
volatility = sqrt(
|
196
|
+
volatility = sqrt(weights.T @ (lg_ret.cov() * per_in_yr @ weights))
|
202
197
|
sr = ret / volatility
|
203
198
|
return cast(NDArray[float64], array([ret, volatility, sr]))
|
204
199
|
|
@@ -107,20 +107,20 @@ class OpenTimeSeries(_CommonModel):
|
|
107
107
|
|
108
108
|
@field_validator("domestic", mode="before")
|
109
109
|
@classmethod
|
110
|
-
def
|
110
|
+
def _validate_domestic(cls, value: CurrencyStringType) -> CurrencyStringType:
|
111
111
|
"""Pydantic validator to ensure domestic field is validated."""
|
112
112
|
_ = Currency(ccy=value)
|
113
113
|
return value
|
114
114
|
|
115
115
|
@field_validator("countries", mode="before")
|
116
116
|
@classmethod
|
117
|
-
def
|
117
|
+
def _validate_countries(cls, value: CountriesType) -> CountriesType:
|
118
118
|
"""Pydantic validator to ensure countries field is validated."""
|
119
119
|
_ = Countries(countryinput=value)
|
120
120
|
return value
|
121
121
|
|
122
122
|
@model_validator(mode="after") # type: ignore[misc,unused-ignore]
|
123
|
-
def
|
123
|
+
def _dates_and_values_validate(self: Self) -> Self:
|
124
124
|
"""Pydantic validator to ensure dates and values are validated."""
|
125
125
|
values_list_length = len(self.values)
|
126
126
|
dates_list_length = len(self.dates)
|
@@ -205,7 +205,7 @@ class OpenTimeSeries(_CommonModel):
|
|
205
205
|
@classmethod
|
206
206
|
def from_df(
|
207
207
|
cls: type[OpenTimeSeries],
|
208
|
-
dframe:
|
208
|
+
dframe: Series[float] | DataFrame,
|
209
209
|
column_nmbr: int = 0,
|
210
210
|
valuetype: ValueType = ValueType.PRICE,
|
211
211
|
baseccy: CurrencyStringType = "SEK",
|
@@ -233,6 +233,7 @@ class OpenTimeSeries(_CommonModel):
|
|
233
233
|
An OpenTimeSeries object
|
234
234
|
|
235
235
|
"""
|
236
|
+
msg = "Argument dframe must be pandas Series or DataFrame."
|
236
237
|
if isinstance(dframe, Series):
|
237
238
|
if isinstance(dframe.name, tuple):
|
238
239
|
label, _ = dframe.name
|
@@ -263,7 +264,6 @@ class OpenTimeSeries(_CommonModel):
|
|
263
264
|
else:
|
264
265
|
label = cast(MultiIndex, dframe.columns).to_numpy()[column_nmbr]
|
265
266
|
else:
|
266
|
-
msg = "Argument dframe must be pandas Series or DataFrame."
|
267
267
|
raise TypeError(msg)
|
268
268
|
|
269
269
|
dates = [date_fix(d).strftime("%Y-%m-%d") for d in dframe.index]
|
@@ -337,9 +337,7 @@ class OpenTimeSeries(_CommonModel):
|
|
337
337
|
)
|
338
338
|
elif not isinstance(d_range, DatetimeIndex) and not all([days, end_dt]):
|
339
339
|
msg = "If d_range is not provided both days and end_dt must be."
|
340
|
-
raise ValueError(
|
341
|
-
msg,
|
342
|
-
)
|
340
|
+
raise ValueError(msg)
|
343
341
|
|
344
342
|
deltas = array(
|
345
343
|
[
|
@@ -436,7 +434,7 @@ class OpenTimeSeries(_CommonModel):
|
|
436
434
|
The returns of the values in the series
|
437
435
|
|
438
436
|
"""
|
439
|
-
self.tsdf = self.tsdf.pct_change(
|
437
|
+
self.tsdf = self.tsdf.pct_change()
|
440
438
|
self.tsdf.iloc[0] = 0
|
441
439
|
self.valuetype = ValueType.RTRN
|
442
440
|
self.tsdf.columns = MultiIndex.from_arrays(
|
@@ -696,7 +694,7 @@ class OpenTimeSeries(_CommonModel):
|
|
696
694
|
returns_input = True
|
697
695
|
else:
|
698
696
|
values = [cast(float, self.tsdf.iloc[0, 0])]
|
699
|
-
ra_df = self.tsdf.pct_change(
|
697
|
+
ra_df = self.tsdf.pct_change()
|
700
698
|
returns_input = False
|
701
699
|
ra_df = ra_df.dropna()
|
702
700
|
|
@@ -122,10 +122,7 @@ class ReturnSimulation(BaseModel):
|
|
122
122
|
"""
|
123
123
|
return cast(
|
124
124
|
float,
|
125
|
-
(
|
126
|
-
self.results.pct_change(fill_method=cast(str, None)).mean()
|
127
|
-
* self.trading_days_in_year
|
128
|
-
).iloc[0],
|
125
|
+
(self.results.pct_change().mean() * self.trading_days_in_year).iloc[0],
|
129
126
|
)
|
130
127
|
|
131
128
|
@property
|
@@ -140,10 +137,9 @@ class ReturnSimulation(BaseModel):
|
|
140
137
|
"""
|
141
138
|
return cast(
|
142
139
|
float,
|
143
|
-
(
|
144
|
-
|
145
|
-
|
146
|
-
).iloc[0],
|
140
|
+
(self.results.pct_change().std() * sqrt(self.trading_days_in_year)).iloc[
|
141
|
+
0
|
142
|
+
],
|
147
143
|
)
|
148
144
|
|
149
145
|
@classmethod
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
import datetime as dt
|
6
6
|
from enum import Enum
|
7
|
+
from pprint import pformat
|
7
8
|
from typing import Annotated, ClassVar, Literal, Union
|
8
9
|
|
9
10
|
from numpy import datetime64
|
@@ -225,8 +226,8 @@ LiteralFrameProps = Literal[
|
|
225
226
|
]
|
226
227
|
|
227
228
|
|
228
|
-
class
|
229
|
-
"""
|
229
|
+
class PropertiesList(list[str]):
|
230
|
+
"""Base class for allowed property arguments definition."""
|
230
231
|
|
231
232
|
allowed_strings: ClassVar[set[str]] = {
|
232
233
|
"value_ret",
|
@@ -246,9 +247,38 @@ class OpenTimeSeriesPropertiesList(list[str]):
|
|
246
247
|
"vol_from_var",
|
247
248
|
"worst",
|
248
249
|
"worst_month",
|
249
|
-
"max_drawdown_cal_year",
|
250
250
|
"max_drawdown",
|
251
251
|
"max_drawdown_date",
|
252
|
+
"max_drawdown_cal_year",
|
253
|
+
}
|
254
|
+
|
255
|
+
def _validate(self: Self) -> None:
|
256
|
+
"""Validate the string input of the all_properties method."""
|
257
|
+
seen = set()
|
258
|
+
invalids = set()
|
259
|
+
duplicates = set()
|
260
|
+
msg = ""
|
261
|
+
for item in self:
|
262
|
+
if item not in self.allowed_strings:
|
263
|
+
invalids.add(item)
|
264
|
+
if item in seen:
|
265
|
+
duplicates.add(item)
|
266
|
+
seen.add(item)
|
267
|
+
if len(invalids) != 0:
|
268
|
+
msg += (
|
269
|
+
f"Invalid string(s): {list(invalids)}.\nAllowed strings are:"
|
270
|
+
f"\n{pformat(self.allowed_strings)}\n"
|
271
|
+
)
|
272
|
+
if len(duplicates) != 0:
|
273
|
+
msg += f"Duplicate string(s): {list(duplicates)}."
|
274
|
+
if len(msg) != 0:
|
275
|
+
raise ValueError(msg)
|
276
|
+
|
277
|
+
|
278
|
+
class OpenTimeSeriesPropertiesList(PropertiesList):
|
279
|
+
"""Allowed property arguments for the OpenTimeSeries class."""
|
280
|
+
|
281
|
+
allowed_strings: ClassVar[set[str]] = PropertiesList.allowed_strings | {
|
252
282
|
"first_idx",
|
253
283
|
"last_idx",
|
254
284
|
"length",
|
@@ -265,46 +295,11 @@ class OpenTimeSeriesPropertiesList(list[str]):
|
|
265
295
|
super().__init__(args)
|
266
296
|
self._validate()
|
267
297
|
|
268
|
-
def _validate(self: Self) -> None:
|
269
|
-
seen = set()
|
270
|
-
for item in self:
|
271
|
-
if item not in self.allowed_strings:
|
272
|
-
msg = (
|
273
|
-
f"Invalid string: {item}. Allowed strings: {self.allowed_strings}"
|
274
|
-
)
|
275
|
-
raise ValueError(
|
276
|
-
msg,
|
277
|
-
)
|
278
|
-
if item in seen:
|
279
|
-
msg = f"Duplicate string: {item}"
|
280
|
-
raise ValueError(msg)
|
281
|
-
seen.add(item)
|
282
|
-
|
283
298
|
|
284
|
-
class OpenFramePropertiesList(
|
299
|
+
class OpenFramePropertiesList(PropertiesList):
|
285
300
|
"""Allowed property arguments for the OpenFrame class."""
|
286
301
|
|
287
|
-
allowed_strings: ClassVar[set[str]] = {
|
288
|
-
"value_ret",
|
289
|
-
"geo_ret",
|
290
|
-
"arithmetic_ret",
|
291
|
-
"vol",
|
292
|
-
"downside_deviation",
|
293
|
-
"ret_vol_ratio",
|
294
|
-
"sortino_ratio",
|
295
|
-
"omega_ratio",
|
296
|
-
"z_score",
|
297
|
-
"skew",
|
298
|
-
"kurtosis",
|
299
|
-
"positive_share",
|
300
|
-
"var_down",
|
301
|
-
"cvar_down",
|
302
|
-
"vol_from_var",
|
303
|
-
"worst",
|
304
|
-
"worst_month",
|
305
|
-
"max_drawdown",
|
306
|
-
"max_drawdown_date",
|
307
|
-
"max_drawdown_cal_year",
|
302
|
+
allowed_strings: ClassVar[set[str]] = PropertiesList.allowed_strings | {
|
308
303
|
"first_indices",
|
309
304
|
"last_indices",
|
310
305
|
"lengths_of_items",
|
@@ -316,21 +311,6 @@ class OpenFramePropertiesList(list[str]):
|
|
316
311
|
super().__init__(args)
|
317
312
|
self._validate()
|
318
313
|
|
319
|
-
def _validate(self: Self) -> None:
|
320
|
-
seen = set()
|
321
|
-
for item in self:
|
322
|
-
if item not in self.allowed_strings:
|
323
|
-
msg = (
|
324
|
-
f"Invalid string: {item}. Allowed strings: {self.allowed_strings}"
|
325
|
-
)
|
326
|
-
raise ValueError(
|
327
|
-
msg,
|
328
|
-
)
|
329
|
-
if item in seen:
|
330
|
-
msg = f"Duplicate string: {item}"
|
331
|
-
raise ValueError(msg)
|
332
|
-
seen.add(item)
|
333
|
-
|
334
314
|
|
335
315
|
class ValueType(str, Enum):
|
336
316
|
"""Enum types of OpenTimeSeries to identify the output."""
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "openseries"
|
3
|
-
version = "1.7.
|
3
|
+
version = "1.7.3"
|
4
4
|
description = "Tools for analyzing financial timeseries."
|
5
5
|
authors = ["Martin Karrin <martin.karrin@captor.se>"]
|
6
6
|
repository = "https://github.com/CaptorAB/openseries"
|
@@ -49,11 +49,11 @@ statsmodels = ">=0.14.0,<1.0.0"
|
|
49
49
|
black = ">=24.4.2,<25.0.0"
|
50
50
|
coverage = ">=7.6.0,<8.0.0"
|
51
51
|
genbadge = {version = ">=1.1.1,<2.0.0", extras = ["coverage"]}
|
52
|
-
mypy = "^1.11.
|
52
|
+
mypy = "^1.11.2"
|
53
53
|
pandas-stubs = ">=2.1.2,<3.0.0"
|
54
54
|
pre-commit = ">=3.7.1,<4.0.0"
|
55
55
|
pytest = ">=8.2.2,<9.0.0"
|
56
|
-
ruff = "^0.5
|
56
|
+
ruff = "^0.6.5"
|
57
57
|
types-openpyxl = ">=3.1.2,<4.0.0"
|
58
58
|
types-python-dateutil = ">=2.8.2,<3.0.0"
|
59
59
|
types-requests = ">=2.20.0,<3.0.0"
|
@@ -79,7 +79,7 @@ output = "coverage.xml"
|
|
79
79
|
exclude = ["venv/*"]
|
80
80
|
strict = true
|
81
81
|
pretty = true
|
82
|
-
warn_unreachable =
|
82
|
+
warn_unreachable = true
|
83
83
|
warn_redundant_casts = true
|
84
84
|
warn_unused_ignores = true
|
85
85
|
disallow_any_generics = true
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|