openseries 2.1.6__tar.gz → 2.1.8__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-2.1.6 → openseries-2.1.8}/PKG-INFO +41 -25
- {openseries-2.1.6 → openseries-2.1.8}/README.md +1 -1
- {openseries-2.1.6 → openseries-2.1.8}/openseries/_common_model.py +130 -24
- {openseries-2.1.6 → openseries-2.1.8}/openseries/frame.py +38 -21
- {openseries-2.1.6 → openseries-2.1.8}/openseries/owntypes.py +6 -0
- {openseries-2.1.6 → openseries-2.1.8}/openseries/portfoliotools.py +25 -11
- {openseries-2.1.6 → openseries-2.1.8}/openseries/report.py +3 -1
- {openseries-2.1.6 → openseries-2.1.8}/openseries/series.py +181 -15
- {openseries-2.1.6 → openseries-2.1.8}/openseries/simulation.py +74 -18
- openseries-2.1.8/openseries.egg-info/PKG-INFO +116 -0
- openseries-2.1.8/openseries.egg-info/SOURCES.txt +31 -0
- openseries-2.1.8/openseries.egg-info/dependency_links.txt +1 -0
- openseries-2.1.8/openseries.egg-info/requires.txt +31 -0
- openseries-2.1.8/openseries.egg-info/top_level.txt +1 -0
- {openseries-2.1.6 → openseries-2.1.8}/pyproject.toml +27 -30
- openseries-2.1.8/setup.cfg +4 -0
- openseries-2.1.8/tests/test_common_model.py +367 -0
- openseries-2.1.8/tests/test_common_model_internals.py +77 -0
- openseries-2.1.8/tests/test_datefixer.py +497 -0
- openseries-2.1.8/tests/test_frame.py +5078 -0
- openseries-2.1.8/tests/test_package.py +71 -0
- openseries-2.1.8/tests/test_portfoliotools.py +852 -0
- openseries-2.1.8/tests/test_report.py +708 -0
- openseries-2.1.8/tests/test_series.py +2258 -0
- openseries-2.1.8/tests/test_simulation.py +326 -0
- openseries-2.1.8/tests/test_types.py +97 -0
- openseries-2.1.6/openseries/plotly_captor_logo.json +0 -9
- openseries-2.1.6/openseries/plotly_layouts.json +0 -75
- {openseries-2.1.6 → openseries-2.1.8}/LICENSE.md +0 -0
- {openseries-2.1.6 → openseries-2.1.8}/openseries/__init__.py +0 -0
- {openseries-2.1.6 → openseries-2.1.8}/openseries/_risk.py +0 -0
- {openseries-2.1.6 → openseries-2.1.8}/openseries/datefixer.py +0 -0
- {openseries-2.1.6 → openseries-2.1.8}/openseries/html_utils.py +0 -0
- {openseries-2.1.6 → openseries-2.1.8}/openseries/load_plotly.py +0 -0
- {openseries-2.1.6 → openseries-2.1.8}/openseries/py.typed +0 -0
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openseries
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.8
|
|
4
4
|
Summary: Tools for analyzing financial timeseries.
|
|
5
|
-
License-Expression: BSD-3-Clause
|
|
6
|
-
License-File: LICENSE.md
|
|
7
|
-
Keywords: python,finance,fintech,data-science,timeseries,timeseries-data,timeseries-analysis,investment,investment-analysis,investing
|
|
8
5
|
Author: Martin Karrin
|
|
9
|
-
Author-email: martin.karrin@captor.se
|
|
10
6
|
Maintainer: Martin Karrin
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
License-Expression: BSD-3-Clause
|
|
8
|
+
Project-URL: Homepage, https://captorab.github.io/openseries/
|
|
9
|
+
Project-URL: Documentation, https://openseries.readthedocs.io/
|
|
10
|
+
Project-URL: Source, https://github.com/CaptorAB/openseries
|
|
11
|
+
Project-URL: Issue Tracker, https://github.com/CaptorAB/openseries/issues
|
|
12
|
+
Project-URL: Release Notes, https://github.com/CaptorAB/openseries/releases
|
|
13
|
+
Keywords: python,finance,fintech,data-science,timeseries,timeseries-data,timeseries-analysis,investment,investment-analysis,investing
|
|
13
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
@@ -20,23 +21,39 @@ Classifier: Natural Language :: English
|
|
|
20
21
|
Classifier: Development Status :: 5 - Production/Stable
|
|
21
22
|
Classifier: Operating System :: OS Independent
|
|
22
23
|
Classifier: Framework :: Pydantic
|
|
23
|
-
Requires-
|
|
24
|
-
Requires-Dist: holidays (>=0.30)
|
|
25
|
-
Requires-Dist: numpy (>=1.23.2)
|
|
26
|
-
Requires-Dist: openpyxl (>=3.1.2)
|
|
27
|
-
Requires-Dist: pandas (>=2.1.2)
|
|
28
|
-
Requires-Dist: plotly (>=5.18.0)
|
|
29
|
-
Requires-Dist: pydantic (>=2.5.2)
|
|
30
|
-
Requires-Dist: python-dateutil (>=2.8.2)
|
|
31
|
-
Requires-Dist: requests (>=2.20.0)
|
|
32
|
-
Requires-Dist: scikit-learn (>=1.4.0)
|
|
33
|
-
Requires-Dist: scipy (>=1.14.1)
|
|
34
|
-
Project-URL: Documentation, https://openseries.readthedocs.io/
|
|
35
|
-
Project-URL: Homepage, https://captorab.github.io/openseries/
|
|
36
|
-
Project-URL: Issue Tracker, https://github.com/CaptorAB/openseries/issues
|
|
37
|
-
Project-URL: Release Notes, https://github.com/CaptorAB/openseries/releases
|
|
38
|
-
Project-URL: Source, https://github.com/CaptorAB/openseries
|
|
24
|
+
Requires-Python: <3.15,>=3.11
|
|
39
25
|
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE.md
|
|
27
|
+
Requires-Dist: exchange-calendars>=4.8
|
|
28
|
+
Requires-Dist: holidays>=0.30
|
|
29
|
+
Requires-Dist: numpy>=1.23.2
|
|
30
|
+
Requires-Dist: openpyxl>=3.1.2
|
|
31
|
+
Requires-Dist: pandas>=2.1.2
|
|
32
|
+
Requires-Dist: plotly>=5.18.0
|
|
33
|
+
Requires-Dist: pydantic>=2.5.2
|
|
34
|
+
Requires-Dist: python-dateutil>=2.8.2
|
|
35
|
+
Requires-Dist: requests>=2.20.0
|
|
36
|
+
Requires-Dist: scipy>=1.14.1
|
|
37
|
+
Requires-Dist: scikit-learn>=1.4.0
|
|
38
|
+
Requires-Dist: tzdata>=2025.3
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: mypy==2.1.0; extra == "dev"
|
|
41
|
+
Requires-Dist: pandas-stubs>=2.1.2; extra == "dev"
|
|
42
|
+
Requires-Dist: pre-commit>=4.5.1; extra == "dev"
|
|
43
|
+
Requires-Dist: pytest>=9.0.3; extra == "dev"
|
|
44
|
+
Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
|
|
45
|
+
Requires-Dist: pytest-xdist>=3.8.0; extra == "dev"
|
|
46
|
+
Requires-Dist: ruff==0.15.14; extra == "dev"
|
|
47
|
+
Requires-Dist: types-openpyxl>=3.1.2; extra == "dev"
|
|
48
|
+
Requires-Dist: scipy-stubs>=1.14.1.0; extra == "dev"
|
|
49
|
+
Requires-Dist: types-python-dateutil>=2.8.2; extra == "dev"
|
|
50
|
+
Requires-Dist: types-requests>=2.20.0; extra == "dev"
|
|
51
|
+
Provides-Extra: docs
|
|
52
|
+
Requires-Dist: sphinx>=9.0.4; extra == "docs"
|
|
53
|
+
Requires-Dist: sphinx-autobuild>=2025.8.25; extra == "docs"
|
|
54
|
+
Requires-Dist: sphinx-autodoc-typehints>=3.6.0; extra == "docs"
|
|
55
|
+
Requires-Dist: sphinx-rtd-theme>=3.1.0rc1; extra == "docs"
|
|
56
|
+
Dynamic: license-file
|
|
40
57
|
|
|
41
58
|
<a href="https://captor.se/"><img src="https://sales.captor.se/captor_logo_sv_1600_icketransparent.png" alt="Captor Fund Management AB" width="81" height="100" align="left" float="right"/></a><br/>
|
|
42
59
|
|
|
@@ -51,7 +68,7 @@ Description-Content-Type: text/markdown
|
|
|
51
68
|
[](https://github.com/CaptorAB/openseries/actions/workflows/test.yml)
|
|
52
69
|
[](https://codecov.io/gh/CaptorAB/openseries/branch/master)
|
|
53
70
|
[](https://captorab.github.io/openseries/)
|
|
54
|
-
[](https://github.com/astral-sh/uv)
|
|
55
72
|
[](https://beta.ruff.rs/docs/)
|
|
56
73
|
[](https://github.com/CaptorAB/openseries/blob/master/LICENSE.md)
|
|
57
74
|
[](https://nbviewer.org/github/karrmagadgeteer2/NoteBook/blob/master/openseriesnotebook.ipynb)
|
|
@@ -97,4 +114,3 @@ _,_=series.plot_series()
|
|
|
97
114
|
### Sample output using the report_html() function
|
|
98
115
|
|
|
99
116
|
<img src="https://raw.githubusercontent.com/CaptorAB/openseries/master/openseries_plot.png" alt="Two Assets Compared" width="1000" />
|
|
100
|
-
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
[](https://github.com/CaptorAB/openseries/actions/workflows/test.yml)
|
|
12
12
|
[](https://codecov.io/gh/CaptorAB/openseries/branch/master)
|
|
13
13
|
[](https://captorab.github.io/openseries/)
|
|
14
|
-
[](https://github.com/astral-sh/uv)
|
|
15
15
|
[](https://beta.ruff.rs/docs/)
|
|
16
16
|
[](https://github.com/CaptorAB/openseries/blob/master/LICENSE.md)
|
|
17
17
|
[](https://nbviewer.org/github/karrmagadgeteer2/NoteBook/blob/master/openseriesnotebook.ipynb)
|
|
@@ -60,7 +60,7 @@ from pandas import (
|
|
|
60
60
|
from pandas.tseries.offsets import CustomBusinessDay
|
|
61
61
|
from plotly.figure_factory import create_distplot # type: ignore[import-untyped]
|
|
62
62
|
from plotly.graph_objs import Figure # type: ignore[import-untyped]
|
|
63
|
-
from pydantic import BaseModel, ConfigDict, DirectoryPath
|
|
63
|
+
from pydantic import BaseModel, ConfigDict, DirectoryPath, Field
|
|
64
64
|
from scipy.stats import (
|
|
65
65
|
kurtosis,
|
|
66
66
|
norm,
|
|
@@ -162,6 +162,30 @@ def _get_base_column_data(
|
|
|
162
162
|
return data, item, label
|
|
163
163
|
|
|
164
164
|
|
|
165
|
+
def _demeaned_returns_for_autocorr(
|
|
166
|
+
series: Series[float], valuetype: ValueType, *, squared: bool = False
|
|
167
|
+
) -> Series[float]:
|
|
168
|
+
"""Return demeaned return series for autocorrelation analysis.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
series: Input series (prices or returns).
|
|
172
|
+
valuetype: ValueType.PRICE for price data (pct_change applied),
|
|
173
|
+
else use as returns.
|
|
174
|
+
squared: If True, square the demeaned returns.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Demeaned return series (optionally squared).
|
|
178
|
+
"""
|
|
179
|
+
if valuetype == ValueType.PRICE:
|
|
180
|
+
rets = series.ffill().pct_change().dropna()
|
|
181
|
+
else:
|
|
182
|
+
rets = series.ffill().dropna()
|
|
183
|
+
rets = rets - rets.mean()
|
|
184
|
+
if squared:
|
|
185
|
+
rets = rets**2
|
|
186
|
+
return rets
|
|
187
|
+
|
|
188
|
+
|
|
165
189
|
def _calculate_time_factor(
|
|
166
190
|
data: Series[float],
|
|
167
191
|
earlier: dt.date,
|
|
@@ -189,6 +213,10 @@ def _calculate_time_factor(
|
|
|
189
213
|
class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
190
214
|
"""Declare _CommonModel."""
|
|
191
215
|
|
|
216
|
+
constituents: list[Any] = Field(default_factory=list)
|
|
217
|
+
weights: list[float] | None = None
|
|
218
|
+
markets: list[str] | str | None = None
|
|
219
|
+
|
|
192
220
|
tsdf: DataFrame = DataFrame(dtype="float64")
|
|
193
221
|
|
|
194
222
|
model_config = ConfigDict(
|
|
@@ -198,18 +226,27 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
198
226
|
)
|
|
199
227
|
|
|
200
228
|
def _coerce_result(
|
|
201
|
-
self: Self,
|
|
229
|
+
self: Self,
|
|
230
|
+
result: Series[float],
|
|
231
|
+
name: str,
|
|
202
232
|
) -> SeriesOrFloat_co:
|
|
203
233
|
if self.tsdf.shape[1] == 1:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
234
|
+
return cast(
|
|
235
|
+
"SeriesOrFloat_co",
|
|
236
|
+
cast("object", float(asarray(a=result, dtype=float64).squeeze())),
|
|
237
|
+
)
|
|
238
|
+
return cast(
|
|
239
|
+
"SeriesOrFloat_co",
|
|
240
|
+
cast(
|
|
241
|
+
"object",
|
|
242
|
+
Series(
|
|
243
|
+
data=result,
|
|
244
|
+
index=self.tsdf.columns,
|
|
245
|
+
name=name,
|
|
246
|
+
dtype="float64",
|
|
247
|
+
),
|
|
248
|
+
),
|
|
211
249
|
)
|
|
212
|
-
return series_result
|
|
213
250
|
|
|
214
251
|
@property
|
|
215
252
|
def length(self: Self) -> int:
|
|
@@ -354,6 +391,23 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
354
391
|
"""
|
|
355
392
|
return self.vol_func()
|
|
356
393
|
|
|
394
|
+
@property
|
|
395
|
+
def autocorr(self: Self) -> SeriesOrFloat_co:
|
|
396
|
+
"""Autocorrelation at lag 1.
|
|
397
|
+
|
|
398
|
+
Shorthand for ``autocorr_func(lag=1)``. Returns the lag-1 autocorrelation
|
|
399
|
+
of demeaned returns. For price series, returns are computed via
|
|
400
|
+
``pct_change``; for return series, raw values are used after demeaning.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
--------
|
|
404
|
+
SeriesOrFloat_co
|
|
405
|
+
Autocorrelation at lag 1.
|
|
406
|
+
Returns float for OpenTimeSeries, Series[float] for OpenFrame.
|
|
407
|
+
|
|
408
|
+
"""
|
|
409
|
+
return self.autocorr_func()
|
|
410
|
+
|
|
357
411
|
@property
|
|
358
412
|
def downside_deviation(self: Self) -> SeriesOrFloat_co:
|
|
359
413
|
"""Downside Deviation.
|
|
@@ -548,7 +602,7 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
548
602
|
"""
|
|
549
603
|
method: LiteralPandasReindexMethod = "nearest"
|
|
550
604
|
|
|
551
|
-
if
|
|
605
|
+
if self.constituents:
|
|
552
606
|
countries = self.constituents[0].countries
|
|
553
607
|
markets = self.constituents[0].markets
|
|
554
608
|
else:
|
|
@@ -749,12 +803,20 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
749
803
|
if hasattr(self, "countries"):
|
|
750
804
|
self.countries = countries
|
|
751
805
|
else:
|
|
752
|
-
|
|
806
|
+
constituents = getattr(self, "constituents", None)
|
|
807
|
+
if not constituents:
|
|
808
|
+
msg = "Cannot set countries without constituents."
|
|
809
|
+
raise TypeError(msg)
|
|
810
|
+
for serie in constituents:
|
|
753
811
|
serie.countries = countries
|
|
754
812
|
elif hasattr(self, "countries"):
|
|
755
813
|
countries = self.countries
|
|
756
814
|
else:
|
|
757
|
-
|
|
815
|
+
constituents = getattr(self, "constituents", None)
|
|
816
|
+
if not constituents:
|
|
817
|
+
msg = "Cannot get countries without constituents."
|
|
818
|
+
raise TypeError(msg)
|
|
819
|
+
countries = constituents[0].countries
|
|
758
820
|
|
|
759
821
|
return countries
|
|
760
822
|
|
|
@@ -770,15 +832,14 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
770
832
|
The markets value after getting or setting.
|
|
771
833
|
"""
|
|
772
834
|
if markets:
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
for serie in self.constituents: # type: ignore[attr-defined]
|
|
835
|
+
constituents = getattr(self, "constituents", None)
|
|
836
|
+
if constituents:
|
|
837
|
+
for serie in constituents:
|
|
777
838
|
serie.markets = markets
|
|
778
|
-
|
|
779
|
-
markets = self.markets
|
|
839
|
+
self.markets = markets
|
|
780
840
|
else:
|
|
781
|
-
|
|
841
|
+
constituents = getattr(self, "constituents", None)
|
|
842
|
+
markets = constituents[0].markets if constituents else self.markets
|
|
782
843
|
|
|
783
844
|
return markets
|
|
784
845
|
|
|
@@ -824,7 +885,7 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
824
885
|
else None,
|
|
825
886
|
)
|
|
826
887
|
]
|
|
827
|
-
self.tsdf = self.tsdf.reindex(labels=d_range, method=method
|
|
888
|
+
self.tsdf = self.tsdf.reindex(labels=d_range, method=method)
|
|
828
889
|
|
|
829
890
|
return self
|
|
830
891
|
|
|
@@ -890,8 +951,8 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
890
951
|
def to_json(
|
|
891
952
|
self: Self,
|
|
892
953
|
what_output: LiteralJsonOutput,
|
|
893
|
-
filename: str,
|
|
894
|
-
directory: DirectoryPath | None = None,
|
|
954
|
+
filename: str | Path,
|
|
955
|
+
directory: DirectoryPath | Path | str | None = None,
|
|
895
956
|
) -> list[dict[str, str | bool | ValueType | list[str] | list[float]]]:
|
|
896
957
|
"""Dump timeseries data into a JSON file.
|
|
897
958
|
|
|
@@ -938,7 +999,8 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
938
999
|
itemdata.update({"values": values})
|
|
939
1000
|
output.append(dict(itemdata))
|
|
940
1001
|
|
|
941
|
-
|
|
1002
|
+
plotfile = dirpath.joinpath(Path(filename).name)
|
|
1003
|
+
with plotfile.open(mode="w", encoding="utf-8") as jsonfile:
|
|
942
1004
|
dump(obj=output, fp=jsonfile, indent=2, sort_keys=False)
|
|
943
1005
|
|
|
944
1006
|
return output
|
|
@@ -1482,6 +1544,50 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
1482
1544
|
|
|
1483
1545
|
return self._coerce_result(result=result, name="Volatility")
|
|
1484
1546
|
|
|
1547
|
+
def autocorr_func(
|
|
1548
|
+
self: Self,
|
|
1549
|
+
lag: int = 1,
|
|
1550
|
+
*,
|
|
1551
|
+
squared: bool = False,
|
|
1552
|
+
) -> SeriesOrFloat_co:
|
|
1553
|
+
"""Calculate autocorrelation at a given lag.
|
|
1554
|
+
|
|
1555
|
+
Computes the autocorrelation of demeaned returns at the specified lag.
|
|
1556
|
+
For price series (ValueType.PRICE), returns are derived via ``pct_change``;
|
|
1557
|
+
for return series (ValueType.RTRN), raw values are demeaned. Use
|
|
1558
|
+
``squared=True`` for squared-return autocorrelation (e.g. volatility
|
|
1559
|
+
clustering). Returns ``nan`` when the series has too few observations.
|
|
1560
|
+
|
|
1561
|
+
Args:
|
|
1562
|
+
lag: The lag at which to compute autocorrelation. Defaults to 1.
|
|
1563
|
+
squared: If True, compute autocorrelation of squared returns.
|
|
1564
|
+
Defaults to False.
|
|
1565
|
+
|
|
1566
|
+
Returns:
|
|
1567
|
+
Autocorrelation at the specified lag. Float for OpenTimeSeries,
|
|
1568
|
+
``Series[float]`` for OpenFrame.
|
|
1569
|
+
"""
|
|
1570
|
+
values: list[float] = []
|
|
1571
|
+
vtypes = self.tsdf.columns.get_level_values(1)
|
|
1572
|
+
for col_idx, col in enumerate(self.tsdf.columns):
|
|
1573
|
+
valuetype = cast("ValueType", vtypes[col_idx])
|
|
1574
|
+
rets = _demeaned_returns_for_autocorr(
|
|
1575
|
+
series=self.tsdf[col],
|
|
1576
|
+
valuetype=valuetype,
|
|
1577
|
+
squared=squared,
|
|
1578
|
+
)
|
|
1579
|
+
if len(rets) > lag:
|
|
1580
|
+
values.append(float(rets.autocorr(lag=lag)))
|
|
1581
|
+
else:
|
|
1582
|
+
values.append(float("nan"))
|
|
1583
|
+
result = Series(
|
|
1584
|
+
data=values,
|
|
1585
|
+
index=self.tsdf.columns,
|
|
1586
|
+
name="Autocorrelation",
|
|
1587
|
+
dtype="float64",
|
|
1588
|
+
)
|
|
1589
|
+
return self._coerce_result(result=result, name="Autocorrelation")
|
|
1590
|
+
|
|
1485
1591
|
def vol_from_var_func(
|
|
1486
1592
|
self: Self,
|
|
1487
1593
|
level: float = 0.95,
|
|
@@ -91,13 +91,10 @@ class OpenFrame(_CommonModel[SeriesFloat]):
|
|
|
91
91
|
weights: List of weights in float format. Optional.
|
|
92
92
|
"""
|
|
93
93
|
|
|
94
|
-
constituents: list[OpenTimeSeries]
|
|
95
|
-
tsdf: DataFrame = DataFrame(dtype="float64")
|
|
96
|
-
weights: list[float] | None = None
|
|
97
|
-
|
|
98
94
|
@field_validator("constituents")
|
|
95
|
+
@classmethod
|
|
99
96
|
def _check_labels_unique(
|
|
100
|
-
cls: type[OpenFrame],
|
|
97
|
+
cls: type[OpenFrame],
|
|
101
98
|
tseries: list[OpenTimeSeries],
|
|
102
99
|
) -> list[OpenTimeSeries]:
|
|
103
100
|
"""Pydantic validator ensuring that OpenFrame labels are unique."""
|
|
@@ -122,7 +119,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
|
|
|
122
119
|
"""
|
|
123
120
|
copied_constituents = [ts.from_deepcopy() for ts in constituents]
|
|
124
121
|
|
|
125
|
-
super().__init__(
|
|
122
|
+
super().__init__(
|
|
126
123
|
constituents=copied_constituents,
|
|
127
124
|
weights=weights,
|
|
128
125
|
)
|
|
@@ -140,6 +137,18 @@ class OpenFrame(_CommonModel[SeriesFloat]):
|
|
|
140
137
|
else:
|
|
141
138
|
logger.warning("OpenFrame() was passed an empty list.")
|
|
142
139
|
|
|
140
|
+
def _coerce_result(
|
|
141
|
+
self: Self,
|
|
142
|
+
result: Series[float],
|
|
143
|
+
name: str,
|
|
144
|
+
) -> SeriesFloat:
|
|
145
|
+
return Series(
|
|
146
|
+
data=result,
|
|
147
|
+
index=self.tsdf.columns,
|
|
148
|
+
name=name,
|
|
149
|
+
dtype="float64",
|
|
150
|
+
)
|
|
151
|
+
|
|
143
152
|
def from_deepcopy(self: Self) -> Self:
|
|
144
153
|
"""Create copy of the OpenFrame object.
|
|
145
154
|
|
|
@@ -309,11 +318,14 @@ class OpenFrame(_CommonModel[SeriesFloat]):
|
|
|
309
318
|
returns = self.tsdf.ffill().pct_change()
|
|
310
319
|
returns.iloc[0] = 0
|
|
311
320
|
new_labels: list[ValueType] = [ValueType.RTRN] * self.item_count
|
|
312
|
-
arrays
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
321
|
+
arrays = cast(
|
|
322
|
+
"Any",
|
|
323
|
+
[
|
|
324
|
+
self.tsdf.columns.get_level_values(0),
|
|
325
|
+
new_labels,
|
|
326
|
+
],
|
|
327
|
+
)
|
|
328
|
+
returns.columns = MultiIndex.from_arrays(arrays)
|
|
317
329
|
self.tsdf = returns.copy()
|
|
318
330
|
return self
|
|
319
331
|
|
|
@@ -330,10 +342,13 @@ class OpenFrame(_CommonModel[SeriesFloat]):
|
|
|
330
342
|
self.tsdf = self.tsdf.diff(periods=periods)
|
|
331
343
|
self.tsdf.iloc[0] = 0
|
|
332
344
|
new_labels: list[ValueType] = [ValueType.RTRN] * self.item_count
|
|
333
|
-
arrays
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
345
|
+
arrays = cast(
|
|
346
|
+
"Any",
|
|
347
|
+
[
|
|
348
|
+
self.tsdf.columns.get_level_values(0),
|
|
349
|
+
new_labels,
|
|
350
|
+
],
|
|
351
|
+
)
|
|
337
352
|
self.tsdf.columns = MultiIndex.from_arrays(arrays)
|
|
338
353
|
return self
|
|
339
354
|
|
|
@@ -358,10 +373,13 @@ class OpenFrame(_CommonModel[SeriesFloat]):
|
|
|
358
373
|
self.tsdf = returns.cumprod(axis=0) / returns.iloc[0]
|
|
359
374
|
|
|
360
375
|
new_labels: list[ValueType] = [ValueType.PRICE] * self.item_count
|
|
361
|
-
arrays
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
376
|
+
arrays = cast(
|
|
377
|
+
"Any",
|
|
378
|
+
[
|
|
379
|
+
self.tsdf.columns.get_level_values(0),
|
|
380
|
+
new_labels,
|
|
381
|
+
],
|
|
382
|
+
)
|
|
365
383
|
self.tsdf.columns = MultiIndex.from_arrays(arrays)
|
|
366
384
|
return self
|
|
367
385
|
|
|
@@ -663,13 +681,12 @@ class OpenFrame(_CommonModel[SeriesFloat]):
|
|
|
663
681
|
if not end_cut and where in ["after", "both"]:
|
|
664
682
|
end_cut = self.last_indices.min()
|
|
665
683
|
self.tsdf = self.tsdf.sort_index()
|
|
666
|
-
self.tsdf = self.tsdf.truncate(before=start_cut, after=end_cut
|
|
684
|
+
self.tsdf = self.tsdf.truncate(before=start_cut, after=end_cut)
|
|
667
685
|
|
|
668
686
|
for xerie in self.constituents:
|
|
669
687
|
xerie.tsdf = xerie.tsdf.truncate(
|
|
670
688
|
before=start_cut,
|
|
671
689
|
after=end_cut,
|
|
672
|
-
copy=False,
|
|
673
690
|
)
|
|
674
691
|
if len(set(self.first_indices)) != 1:
|
|
675
692
|
msg = (
|
|
@@ -184,11 +184,14 @@ LiteralSeriesProps = Literal[
|
|
|
184
184
|
"span_of_days",
|
|
185
185
|
"yearfrac",
|
|
186
186
|
"periods_in_a_year",
|
|
187
|
+
"autocorr",
|
|
188
|
+
"partial_autocorr",
|
|
187
189
|
]
|
|
188
190
|
LiteralFrameProps = Literal[
|
|
189
191
|
"value_ret",
|
|
190
192
|
"geo_ret",
|
|
191
193
|
"arithmetic_ret",
|
|
194
|
+
"autocorr",
|
|
192
195
|
"vol",
|
|
193
196
|
"downside_deviation",
|
|
194
197
|
"ret_vol_ratio",
|
|
@@ -273,6 +276,8 @@ class OpenTimeSeriesPropertiesList(PropertiesList):
|
|
|
273
276
|
"span_of_days",
|
|
274
277
|
"yearfrac",
|
|
275
278
|
"periods_in_a_year",
|
|
279
|
+
"autocorr",
|
|
280
|
+
"partial_autocorr",
|
|
276
281
|
}
|
|
277
282
|
|
|
278
283
|
def __init__(
|
|
@@ -288,6 +293,7 @@ class OpenFramePropertiesList(PropertiesList):
|
|
|
288
293
|
"""Allowed property arguments for the OpenFrame class."""
|
|
289
294
|
|
|
290
295
|
allowed_strings: ClassVar[set[str]] = PropertiesList.allowed_strings | {
|
|
296
|
+
"autocorr",
|
|
291
297
|
"first_indices",
|
|
292
298
|
"last_indices",
|
|
293
299
|
"lengths_of_items",
|
|
@@ -235,8 +235,8 @@ def _build_frontier_line(
|
|
|
235
235
|
|
|
236
236
|
for possible_return in frontier_y:
|
|
237
237
|
cons = cast(
|
|
238
|
-
"
|
|
239
|
-
|
|
238
|
+
"Any",
|
|
239
|
+
[
|
|
240
240
|
{"type": "eq", "fun": _check_sum},
|
|
241
241
|
{
|
|
242
242
|
"type": "eq",
|
|
@@ -247,10 +247,10 @@ def _build_frontier_line(
|
|
|
247
247
|
poss_return=poss_return,
|
|
248
248
|
),
|
|
249
249
|
},
|
|
250
|
-
|
|
250
|
+
],
|
|
251
251
|
)
|
|
252
252
|
|
|
253
|
-
result = minimize(
|
|
253
|
+
result = minimize(
|
|
254
254
|
fun=_minimize_volatility,
|
|
255
255
|
x0=init_guess,
|
|
256
256
|
method=minimize_method,
|
|
@@ -375,8 +375,8 @@ def _optimize_max_sharpe_portfolio(
|
|
|
375
375
|
Returns:
|
|
376
376
|
Tuple of (optimal metrics, optimal weights).
|
|
377
377
|
"""
|
|
378
|
-
constraints = {"type": "eq", "fun": _check_sum}
|
|
379
|
-
opt_results = minimize(
|
|
378
|
+
constraints = cast("Any", [{"type": "eq", "fun": _check_sum}])
|
|
379
|
+
opt_results = minimize(
|
|
380
380
|
fun=_neg_sharpe,
|
|
381
381
|
x0=init_guess,
|
|
382
382
|
method=minimize_method,
|
|
@@ -578,9 +578,18 @@ def prepare_plot_data(
|
|
|
578
578
|
index=["ret", "stdev", "text"],
|
|
579
579
|
)
|
|
580
580
|
plotframe.columns = plotframe.columns.droplevel(level=1)
|
|
581
|
-
plotframe["Max Sharpe Portfolio"] =
|
|
581
|
+
plotframe["Max Sharpe Portfolio"] = Series(
|
|
582
|
+
data=[optimized[0], optimized[1], opt_text],
|
|
583
|
+
index=plotframe.index,
|
|
584
|
+
dtype=object,
|
|
585
|
+
)
|
|
582
586
|
if current.label is not None:
|
|
583
|
-
|
|
587
|
+
label = current.label
|
|
588
|
+
plotframe[label] = Series(
|
|
589
|
+
data=[current.arithmetic_ret, current.vol, txt],
|
|
590
|
+
index=plotframe.index,
|
|
591
|
+
dtype=object,
|
|
592
|
+
)
|
|
584
593
|
|
|
585
594
|
return plotframe
|
|
586
595
|
|
|
@@ -686,9 +695,14 @@ def _add_point_frame_traces(
|
|
|
686
695
|
"dict[str, str | int | float | bool | list[str]]",
|
|
687
696
|
fig["layout"],
|
|
688
697
|
)
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
698
|
+
base_colorway = cast("list[str]", layout_dict.get("colorway", []))
|
|
699
|
+
if len(base_colorway) < len(point_frame.columns) and base_colorway:
|
|
700
|
+
repeats = (len(point_frame.columns) + len(base_colorway) - 1) // len(
|
|
701
|
+
base_colorway
|
|
702
|
+
)
|
|
703
|
+
colorway = (base_colorway * repeats)[: len(point_frame.columns)]
|
|
704
|
+
else:
|
|
705
|
+
colorway = base_colorway[: len(point_frame.columns)]
|
|
692
706
|
for col, clr in zip(point_frame.columns, colorway, strict=True):
|
|
693
707
|
returns.extend([cast("float", point_frame.loc["ret", col])])
|
|
694
708
|
risk.extend([cast("float", point_frame.loc["stdev", col])])
|
|
@@ -536,7 +536,7 @@ def report_html(
|
|
|
536
536
|
*,
|
|
537
537
|
auto_open: bool = False,
|
|
538
538
|
add_logo: bool = True,
|
|
539
|
-
vertical_legend: bool = True,
|
|
539
|
+
vertical_legend: bool = True,
|
|
540
540
|
) -> tuple[Figure, str]:
|
|
541
541
|
"""Generate a responsive HTML report page with line and bar plots and a table."""
|
|
542
542
|
copied = data.from_deepcopy()
|
|
@@ -649,6 +649,8 @@ def report_html(
|
|
|
649
649
|
"config": config,
|
|
650
650
|
}
|
|
651
651
|
|
|
652
|
+
if not vertical_legend:
|
|
653
|
+
logger.debug("Horizontal legend layout requested.")
|
|
652
654
|
legend_html = _get_legend_html(line_traces=line_traces, colorway=colorway)
|
|
653
655
|
|
|
654
656
|
html = _generate_html(
|