openseries 2.1.7__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.7 → openseries-2.1.8}/PKG-INFO +41 -26
  2. {openseries-2.1.7 → openseries-2.1.8}/README.md +1 -1
  3. {openseries-2.1.7 → openseries-2.1.8}/openseries/_common_model.py +44 -23
  4. {openseries-2.1.7 → openseries-2.1.8}/openseries/frame.py +37 -19
  5. {openseries-2.1.7 → openseries-2.1.8}/openseries/portfoliotools.py +25 -11
  6. {openseries-2.1.7 → openseries-2.1.8}/openseries/report.py +3 -1
  7. {openseries-2.1.7 → openseries-2.1.8}/openseries/series.py +22 -12
  8. openseries-2.1.8/openseries.egg-info/PKG-INFO +116 -0
  9. openseries-2.1.8/openseries.egg-info/SOURCES.txt +31 -0
  10. openseries-2.1.8/openseries.egg-info/dependency_links.txt +1 -0
  11. openseries-2.1.8/openseries.egg-info/requires.txt +31 -0
  12. openseries-2.1.8/openseries.egg-info/top_level.txt +1 -0
  13. {openseries-2.1.7 → openseries-2.1.8}/pyproject.toml +25 -29
  14. openseries-2.1.8/setup.cfg +4 -0
  15. openseries-2.1.8/tests/test_common_model.py +367 -0
  16. openseries-2.1.8/tests/test_common_model_internals.py +77 -0
  17. openseries-2.1.8/tests/test_datefixer.py +497 -0
  18. openseries-2.1.8/tests/test_frame.py +5078 -0
  19. openseries-2.1.8/tests/test_package.py +71 -0
  20. openseries-2.1.8/tests/test_portfoliotools.py +852 -0
  21. openseries-2.1.8/tests/test_report.py +708 -0
  22. openseries-2.1.8/tests/test_series.py +2258 -0
  23. openseries-2.1.8/tests/test_simulation.py +326 -0
  24. openseries-2.1.8/tests/test_types.py +97 -0
  25. openseries-2.1.7/openseries/plotly_captor_logo.json +0 -9
  26. openseries-2.1.7/openseries/plotly_layouts.json +0 -75
  27. {openseries-2.1.7 → openseries-2.1.8}/LICENSE.md +0 -0
  28. {openseries-2.1.7 → openseries-2.1.8}/openseries/__init__.py +0 -0
  29. {openseries-2.1.7 → openseries-2.1.8}/openseries/_risk.py +0 -0
  30. {openseries-2.1.7 → openseries-2.1.8}/openseries/datefixer.py +0 -0
  31. {openseries-2.1.7 → openseries-2.1.8}/openseries/html_utils.py +0 -0
  32. {openseries-2.1.7 → openseries-2.1.8}/openseries/load_plotly.py +0 -0
  33. {openseries-2.1.7 → openseries-2.1.8}/openseries/owntypes.py +0 -0
  34. {openseries-2.1.7 → openseries-2.1.8}/openseries/py.typed +0 -0
  35. {openseries-2.1.7 → openseries-2.1.8}/openseries/simulation.py +0 -0
@@ -1,15 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openseries
3
- Version: 2.1.7
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,24 +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
- Requires-Dist: tzdata (>=2025.3)
35
- Project-URL: Documentation, https://openseries.readthedocs.io/
36
- Project-URL: Homepage, https://captorab.github.io/openseries/
37
- Project-URL: Issue Tracker, https://github.com/CaptorAB/openseries/issues
38
- Project-URL: Release Notes, https://github.com/CaptorAB/openseries/releases
39
- Project-URL: Source, https://github.com/CaptorAB/openseries
24
+ Requires-Python: <3.15,>=3.11
40
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
41
57
 
42
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/>
43
59
 
@@ -52,7 +68,7 @@ Description-Content-Type: text/markdown
52
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)
53
69
  [![codecov](https://img.shields.io/codecov/c/gh/CaptorAB/openseries?logo=codecov)](https://codecov.io/gh/CaptorAB/openseries/branch/master)
54
70
  [![Documentation Status](https://readthedocs.org/projects/openseries/badge/?version=latest)](https://captorab.github.io/openseries/)
55
- [![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)
56
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/)
57
73
  [![GitHub License](https://img.shields.io/github/license/CaptorAB/openseries)](https://github.com/CaptorAB/openseries/blob/master/LICENSE.md)
58
74
  [![Code Sample](https://img.shields.io/badge/-Code%20Sample-blue)](https://nbviewer.org/github/karrmagadgeteer2/NoteBook/blob/master/openseriesnotebook.ipynb)
@@ -98,4 +114,3 @@ _,_=series.plot_series()
98
114
  ### Sample output using the report_html() function
99
115
 
100
116
  <img src="https://raw.githubusercontent.com/CaptorAB/openseries/master/openseries_plot.png" alt="Two Assets Compared" width="1000" />
101
-
@@ -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,
@@ -213,6 +213,10 @@ def _calculate_time_factor(
213
213
  class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
214
214
  """Declare _CommonModel."""
215
215
 
216
+ constituents: list[Any] = Field(default_factory=list)
217
+ weights: list[float] | None = None
218
+ markets: list[str] | str | None = None
219
+
216
220
  tsdf: DataFrame = DataFrame(dtype="float64")
217
221
 
218
222
  model_config = ConfigDict(
@@ -222,18 +226,27 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
222
226
  )
223
227
 
224
228
  def _coerce_result(
225
- self: Self, result: Series[float], name: str
229
+ self: Self,
230
+ result: Series[float],
231
+ name: str,
226
232
  ) -> SeriesOrFloat_co:
227
233
  if self.tsdf.shape[1] == 1:
228
- arr = float(asarray(a=result, dtype=float64).squeeze())
229
- return cast("SeriesOrFloat_co", arr) # type: ignore[redundant-cast]
230
- series_result: SeriesOrFloat_co = Series( # type: ignore[assignment]
231
- data=result,
232
- index=self.tsdf.columns,
233
- name=name,
234
- 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
+ ),
235
249
  )
236
- return series_result
237
250
 
238
251
  @property
239
252
  def length(self: Self) -> int:
@@ -589,7 +602,7 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
589
602
  """
590
603
  method: LiteralPandasReindexMethod = "nearest"
591
604
 
592
- if hasattr(self, "constituents"):
605
+ if self.constituents:
593
606
  countries = self.constituents[0].countries
594
607
  markets = self.constituents[0].markets
595
608
  else:
@@ -790,12 +803,20 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
790
803
  if hasattr(self, "countries"):
791
804
  self.countries = countries
792
805
  else:
793
- 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:
794
811
  serie.countries = countries
795
812
  elif hasattr(self, "countries"):
796
813
  countries = self.countries
797
814
  else:
798
- 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
799
820
 
800
821
  return countries
801
822
 
@@ -811,15 +832,14 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
811
832
  The markets value after getting or setting.
812
833
  """
813
834
  if markets:
814
- if hasattr(self, "markets"):
815
- self.markets = markets
816
- else:
817
- for serie in self.constituents: # type: ignore[attr-defined]
835
+ constituents = getattr(self, "constituents", None)
836
+ if constituents:
837
+ for serie in constituents:
818
838
  serie.markets = markets
819
- elif hasattr(self, "markets"):
820
- markets = self.markets
839
+ self.markets = markets
821
840
  else:
822
- 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
823
843
 
824
844
  return markets
825
845
 
@@ -931,8 +951,8 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
931
951
  def to_json(
932
952
  self: Self,
933
953
  what_output: LiteralJsonOutput,
934
- filename: str,
935
- directory: DirectoryPath | None = None,
954
+ filename: str | Path,
955
+ directory: DirectoryPath | Path | str | None = None,
936
956
  ) -> list[dict[str, str | bool | ValueType | list[str] | list[float]]]:
937
957
  """Dump timeseries data into a JSON file.
938
958
 
@@ -979,7 +999,8 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
979
999
  itemdata.update({"values": values})
980
1000
  output.append(dict(itemdata))
981
1001
 
982
- 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:
983
1004
  dump(obj=output, fp=jsonfile, indent=2, sort_keys=False)
984
1005
 
985
1006
  return output
@@ -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
 
@@ -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(
@@ -14,6 +14,7 @@ if TYPE_CHECKING: # pragma: no cover
14
14
  from numpy import (
15
15
  append,
16
16
  array,
17
+ asarray,
17
18
  cumprod,
18
19
  diff,
19
20
  float64,
@@ -107,7 +108,6 @@ class OpenTimeSeries(_CommonModel[float]):
107
108
  currency: CurrencyStringType
108
109
  domestic: CurrencyStringType = "SEK"
109
110
  countries: CountriesType = "SE"
110
- markets: list[str] | str | None = None # type: ignore[assignment]
111
111
  isin: str | None = None
112
112
  label: str | None = None
113
113
 
@@ -173,6 +173,14 @@ class OpenTimeSeries(_CommonModel[float]):
173
173
  raise ValueError(msg)
174
174
  return self
175
175
 
176
+ def _coerce_result(
177
+ self: Self,
178
+ result: Series[float],
179
+ name: str,
180
+ ) -> float:
181
+ _ = name
182
+ return float(asarray(a=result, dtype=float64).squeeze())
183
+
176
184
  @classmethod
177
185
  def from_arrays(
178
186
  cls,
@@ -228,7 +236,7 @@ class OpenTimeSeries(_CommonModel[float]):
228
236
  @classmethod
229
237
  def from_df(
230
238
  cls,
231
- dframe: Series[float] | DataFrame,
239
+ dframe: Series | DataFrame | object,
232
240
  column_nmbr: int = 0,
233
241
  valuetype: ValueType = ValueType.PRICE,
234
242
  baseccy: CurrencyStringType = "SEK",
@@ -255,13 +263,16 @@ class OpenTimeSeries(_CommonModel[float]):
255
263
  """
256
264
  msg = "Argument dframe must be pandas Series or DataFrame."
257
265
  values: list[float]
266
+ pandas_obj: Series | DataFrame
258
267
  if isinstance(dframe, Series):
268
+ pandas_obj = dframe
259
269
  if isinstance(dframe.name, tuple):
260
270
  label, _ = dframe.name
261
271
  else:
262
272
  label = dframe.name
263
273
  values = dframe.to_numpy().tolist()
264
274
  elif isinstance(dframe, DataFrame):
275
+ pandas_obj = dframe
265
276
  values = dframe.iloc[:, column_nmbr].to_list()
266
277
  if isinstance(dframe.columns, MultiIndex):
267
278
  if _check_if_none(
@@ -287,7 +298,7 @@ class OpenTimeSeries(_CommonModel[float]):
287
298
  else:
288
299
  raise TypeError(msg)
289
300
 
290
- dates = [date_fix(d).strftime("%Y-%m-%d") for d in dframe.index]
301
+ dates = [date_fix(d).strftime("%Y-%m-%d") for d in pandas_obj.index]
291
302
 
292
303
  return cls(
293
304
  timeseries_id="",
@@ -444,10 +455,8 @@ class OpenTimeSeries(_CommonModel[float]):
444
455
  returns = self.tsdf.ffill().pct_change()
445
456
  returns.iloc[0] = 0
446
457
  self.valuetype = ValueType.RTRN
447
- arrays = [[self.label], [self.valuetype]]
448
- returns.columns = MultiIndex.from_arrays(
449
- arrays=arrays, # type: ignore[arg-type]
450
- )
458
+ arrays = cast("Any", [[self.label], [self.valuetype]])
459
+ returns.columns = MultiIndex.from_arrays(arrays)
451
460
  self.tsdf = returns.copy()
452
461
  return self
453
462
 
@@ -1045,7 +1054,7 @@ def timeseries_chain(
1045
1054
  )
1046
1055
 
1047
1056
 
1048
- def _check_if_none(item: Any) -> bool: # noqa: ANN401
1057
+ def _check_if_none(item: object) -> bool:
1049
1058
  """Check if a variable is None or equivalent.
1050
1059
 
1051
1060
  Args:
@@ -1054,9 +1063,10 @@ def _check_if_none(item: Any) -> bool: # noqa: ANN401
1054
1063
  Returns:
1055
1064
  Answer to whether the variable is None or equivalent.
1056
1065
  """
1066
+ if item is None:
1067
+ return True
1068
+
1057
1069
  try:
1058
- return cast("bool", isnan(item))
1059
- except TypeError:
1060
- if item is None:
1061
- return True
1070
+ return cast("bool", isnan(cast("float", item)))
1071
+ except (TypeError, ValueError):
1062
1072
  return len(str(item)) == 0
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: openseries
3
+ Version: 2.1.8
4
+ Summary: Tools for analyzing financial timeseries.
5
+ Author: Martin Karrin
6
+ Maintainer: Martin Karrin
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
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Intended Audience :: Financial and Insurance Industry
19
+ Classifier: Topic :: Office/Business :: Financial :: Investment
20
+ Classifier: Natural Language :: English
21
+ Classifier: Development Status :: 5 - Production/Stable
22
+ Classifier: Operating System :: OS Independent
23
+ Classifier: Framework :: Pydantic
24
+ Requires-Python: <3.15,>=3.11
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
57
+
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/>
59
+
60
+ <br><br>
61
+
62
+ # openseries
63
+
64
+ [![PyPI version](https://img.shields.io/pypi/v/openseries.svg)](https://pypi.org/project/openseries/)
65
+ [![Conda Version](https://img.shields.io/conda/vn/conda-forge/openseries.svg)](https://anaconda.org/conda-forge/openseries)
66
+ ![Platform](https://img.shields.io/badge/platforms-Windows%20%7C%20macOS%20%7C%20Linux-blue)
67
+ [![Python version](https://img.shields.io/pypi/pyversions/openseries.svg)](https://www.python.org/)
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)
69
+ [![codecov](https://img.shields.io/codecov/c/gh/CaptorAB/openseries?logo=codecov)](https://codecov.io/gh/CaptorAB/openseries/branch/master)
70
+ [![Documentation Status](https://readthedocs.org/projects/openseries/badge/?version=latest)](https://captorab.github.io/openseries/)
71
+ [![uv](https://img.shields.io/badge/package%20manager-uv-blueviolet)](https://github.com/astral-sh/uv)
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/)
73
+ [![GitHub License](https://img.shields.io/github/license/CaptorAB/openseries)](https://github.com/CaptorAB/openseries/blob/master/LICENSE.md)
74
+ [![Code Sample](https://img.shields.io/badge/-Code%20Sample-blue)](https://nbviewer.org/github/karrmagadgeteer2/NoteBook/blob/master/openseriesnotebook.ipynb)
75
+
76
+ Tools for analyzing financial timeseries of a single asset or a group of assets. Designed for daily or less frequent data.
77
+
78
+ ## Documentation
79
+
80
+ Complete documentation is available at: [https://captorab.github.io/openseries/](https://captorab.github.io/openseries/)
81
+
82
+ The documentation includes:
83
+
84
+ - Quick start guide
85
+ - API reference
86
+ - Tutorials and examples
87
+ - Installation instructions
88
+
89
+ ## Installation
90
+
91
+ ```bash
92
+ pip install openseries
93
+ ```
94
+
95
+ or:
96
+
97
+ ```bash
98
+ conda install -c conda-forge openseries
99
+ ```
100
+
101
+ ## Quick Start
102
+
103
+ ```python
104
+ from openseries import OpenTimeSeries
105
+ import yfinance as yf
106
+
107
+ move=yf.Ticker(ticker="^MOVE")
108
+ history=move.history(period="max")
109
+ series=OpenTimeSeries.from_df(dframe=history.loc[:, "Close"])
110
+ _=series.set_new_label(lvl_zero="ICE BofAML MOVE Index")
111
+ _,_=series.plot_series()
112
+ ```
113
+
114
+ ### Sample output using the report_html() function
115
+
116
+ <img src="https://raw.githubusercontent.com/CaptorAB/openseries/master/openseries_plot.png" alt="Two Assets Compared" width="1000" />