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.
Files changed (35) hide show
  1. {openseries-2.1.6 → openseries-2.1.8}/PKG-INFO +41 -25
  2. {openseries-2.1.6 → openseries-2.1.8}/README.md +1 -1
  3. {openseries-2.1.6 → openseries-2.1.8}/openseries/_common_model.py +130 -24
  4. {openseries-2.1.6 → openseries-2.1.8}/openseries/frame.py +38 -21
  5. {openseries-2.1.6 → openseries-2.1.8}/openseries/owntypes.py +6 -0
  6. {openseries-2.1.6 → openseries-2.1.8}/openseries/portfoliotools.py +25 -11
  7. {openseries-2.1.6 → openseries-2.1.8}/openseries/report.py +3 -1
  8. {openseries-2.1.6 → openseries-2.1.8}/openseries/series.py +181 -15
  9. {openseries-2.1.6 → openseries-2.1.8}/openseries/simulation.py +74 -18
  10. openseries-2.1.8/openseries.egg-info/PKG-INFO +116 -0
  11. openseries-2.1.8/openseries.egg-info/SOURCES.txt +31 -0
  12. openseries-2.1.8/openseries.egg-info/dependency_links.txt +1 -0
  13. openseries-2.1.8/openseries.egg-info/requires.txt +31 -0
  14. openseries-2.1.8/openseries.egg-info/top_level.txt +1 -0
  15. {openseries-2.1.6 → openseries-2.1.8}/pyproject.toml +27 -30
  16. openseries-2.1.8/setup.cfg +4 -0
  17. openseries-2.1.8/tests/test_common_model.py +367 -0
  18. openseries-2.1.8/tests/test_common_model_internals.py +77 -0
  19. openseries-2.1.8/tests/test_datefixer.py +497 -0
  20. openseries-2.1.8/tests/test_frame.py +5078 -0
  21. openseries-2.1.8/tests/test_package.py +71 -0
  22. openseries-2.1.8/tests/test_portfoliotools.py +852 -0
  23. openseries-2.1.8/tests/test_report.py +708 -0
  24. openseries-2.1.8/tests/test_series.py +2258 -0
  25. openseries-2.1.8/tests/test_simulation.py +326 -0
  26. openseries-2.1.8/tests/test_types.py +97 -0
  27. openseries-2.1.6/openseries/plotly_captor_logo.json +0 -9
  28. openseries-2.1.6/openseries/plotly_layouts.json +0 -75
  29. {openseries-2.1.6 → openseries-2.1.8}/LICENSE.md +0 -0
  30. {openseries-2.1.6 → openseries-2.1.8}/openseries/__init__.py +0 -0
  31. {openseries-2.1.6 → openseries-2.1.8}/openseries/_risk.py +0 -0
  32. {openseries-2.1.6 → openseries-2.1.8}/openseries/datefixer.py +0 -0
  33. {openseries-2.1.6 → openseries-2.1.8}/openseries/html_utils.py +0 -0
  34. {openseries-2.1.6 → openseries-2.1.8}/openseries/load_plotly.py +0 -0
  35. {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.6
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
- Maintainer-email: martin.karrin@captor.se
12
- Requires-Python: >=3.11,<3.15
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-Dist: exchange-calendars (>=4.8)
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
  [![GitHub Action Test Suite](https://github.com/CaptorAB/openseries/actions/workflows/test.yml/badge.svg)](https://github.com/CaptorAB/openseries/actions/workflows/test.yml)
52
69
  [![codecov](https://img.shields.io/codecov/c/gh/CaptorAB/openseries?logo=codecov)](https://codecov.io/gh/CaptorAB/openseries/branch/master)
53
70
  [![Documentation Status](https://readthedocs.org/projects/openseries/badge/?version=latest)](https://captorab.github.io/openseries/)
54
- [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
71
+ [![uv](https://img.shields.io/badge/package%20manager-uv-blueviolet)](https://github.com/astral-sh/uv)
55
72
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://beta.ruff.rs/docs/)
56
73
  [![GitHub License](https://img.shields.io/github/license/CaptorAB/openseries)](https://github.com/CaptorAB/openseries/blob/master/LICENSE.md)
57
74
  [![Code Sample](https://img.shields.io/badge/-Code%20Sample-blue)](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
  [![GitHub Action Test Suite](https://github.com/CaptorAB/openseries/actions/workflows/test.yml/badge.svg)](https://github.com/CaptorAB/openseries/actions/workflows/test.yml)
12
12
  [![codecov](https://img.shields.io/codecov/c/gh/CaptorAB/openseries?logo=codecov)](https://codecov.io/gh/CaptorAB/openseries/branch/master)
13
13
  [![Documentation Status](https://readthedocs.org/projects/openseries/badge/?version=latest)](https://captorab.github.io/openseries/)
14
- [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
14
+ [![uv](https://img.shields.io/badge/package%20manager-uv-blueviolet)](https://github.com/astral-sh/uv)
15
15
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://beta.ruff.rs/docs/)
16
16
  [![GitHub License](https://img.shields.io/github/license/CaptorAB/openseries)](https://github.com/CaptorAB/openseries/blob/master/LICENSE.md)
17
17
  [![Code Sample](https://img.shields.io/badge/-Code%20Sample-blue)](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, result: Series[float], name: str
229
+ self: Self,
230
+ result: Series[float],
231
+ name: str,
202
232
  ) -> SeriesOrFloat_co:
203
233
  if self.tsdf.shape[1] == 1:
204
- arr = float(asarray(a=result, dtype=float64).squeeze())
205
- return cast("SeriesOrFloat_co", arr) # type: ignore[redundant-cast]
206
- series_result: SeriesOrFloat_co = Series( # type: ignore[assignment]
207
- data=result,
208
- index=self.tsdf.columns,
209
- name=name,
210
- dtype="float64",
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 hasattr(self, "constituents"):
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
- for serie in self.constituents: # type: ignore[attr-defined]
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
- countries = self.constituents[0].countries # type: ignore[attr-defined]
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
- if hasattr(self, "markets"):
774
- self.markets = markets
775
- else:
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
- elif hasattr(self, "markets"):
779
- markets = self.markets
839
+ self.markets = markets
780
840
  else:
781
- markets = self.constituents[0].markets # type: ignore[attr-defined]
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, copy=False)
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
- with dirpath.joinpath(filename).open(mode="w", encoding="utf-8") as jsonfile:
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], # noqa: N805
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__( # type: ignore[call-arg]
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: list[Index[Any], list[ValueType]] = [ # type: ignore[type-arg]
313
- self.tsdf.columns.get_level_values(0),
314
- new_labels,
315
- ]
316
- returns.columns = MultiIndex.from_arrays(arrays=arrays)
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: list[Index[Any], list[ValueType]] = [ # type: ignore[type-arg]
334
- self.tsdf.columns.get_level_values(0),
335
- new_labels,
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: list[Index[Any], list[ValueType]] = [ # type: ignore[type-arg]
362
- self.tsdf.columns.get_level_values(0),
363
- new_labels,
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, copy=False)
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
- "dict[str, str | Callable[[float, NDArray[float64]], float64]]",
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( # type: ignore[call-overload]
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( # type: ignore[call-overload]
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"] = [optimized[0], optimized[1], opt_text]
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
- plotframe[current.label] = [current.arithmetic_ret, current.vol, txt] # type: ignore[assignment]
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
- colorway = cast("list[str]", layout_dict.get("colorway", []))[
690
- : len(point_frame.columns)
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, # noqa: ARG001
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(