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.
- {openseries-2.1.7 → openseries-2.1.8}/PKG-INFO +41 -26
- {openseries-2.1.7 → openseries-2.1.8}/README.md +1 -1
- {openseries-2.1.7 → openseries-2.1.8}/openseries/_common_model.py +44 -23
- {openseries-2.1.7 → openseries-2.1.8}/openseries/frame.py +37 -19
- {openseries-2.1.7 → openseries-2.1.8}/openseries/portfoliotools.py +25 -11
- {openseries-2.1.7 → openseries-2.1.8}/openseries/report.py +3 -1
- {openseries-2.1.7 → openseries-2.1.8}/openseries/series.py +22 -12
- 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.7 → openseries-2.1.8}/pyproject.toml +25 -29
- 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.7/openseries/plotly_captor_logo.json +0 -9
- openseries-2.1.7/openseries/plotly_layouts.json +0 -75
- {openseries-2.1.7 → openseries-2.1.8}/LICENSE.md +0 -0
- {openseries-2.1.7 → openseries-2.1.8}/openseries/__init__.py +0 -0
- {openseries-2.1.7 → openseries-2.1.8}/openseries/_risk.py +0 -0
- {openseries-2.1.7 → openseries-2.1.8}/openseries/datefixer.py +0 -0
- {openseries-2.1.7 → openseries-2.1.8}/openseries/html_utils.py +0 -0
- {openseries-2.1.7 → openseries-2.1.8}/openseries/load_plotly.py +0 -0
- {openseries-2.1.7 → openseries-2.1.8}/openseries/owntypes.py +0 -0
- {openseries-2.1.7 → openseries-2.1.8}/openseries/py.typed +0 -0
- {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.
|
|
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,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-
|
|
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
|
[](https://github.com/CaptorAB/openseries/actions/workflows/test.yml)
|
|
53
69
|
[](https://codecov.io/gh/CaptorAB/openseries/branch/master)
|
|
54
70
|
[](https://captorab.github.io/openseries/)
|
|
55
|
-
[](https://github.com/astral-sh/uv)
|
|
56
72
|
[](https://beta.ruff.rs/docs/)
|
|
57
73
|
[](https://github.com/CaptorAB/openseries/blob/master/LICENSE.md)
|
|
58
74
|
[](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
|
[](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,
|
|
@@ -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,
|
|
229
|
+
self: Self,
|
|
230
|
+
result: Series[float],
|
|
231
|
+
name: str,
|
|
226
232
|
) -> SeriesOrFloat_co:
|
|
227
233
|
if self.tsdf.shape[1] == 1:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
-
|
|
820
|
-
markets = self.markets
|
|
839
|
+
self.markets = markets
|
|
821
840
|
else:
|
|
822
|
-
|
|
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
|
-
|
|
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],
|
|
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
|
|
|
@@ -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(
|
|
@@ -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
|
|
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
|
|
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:
|
|
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
|
+
[](https://pypi.org/project/openseries/)
|
|
65
|
+
[](https://anaconda.org/conda-forge/openseries)
|
|
66
|
+

|
|
67
|
+
[](https://www.python.org/)
|
|
68
|
+
[](https://github.com/CaptorAB/openseries/actions/workflows/test.yml)
|
|
69
|
+
[](https://codecov.io/gh/CaptorAB/openseries/branch/master)
|
|
70
|
+
[](https://captorab.github.io/openseries/)
|
|
71
|
+
[](https://github.com/astral-sh/uv)
|
|
72
|
+
[](https://beta.ruff.rs/docs/)
|
|
73
|
+
[](https://github.com/CaptorAB/openseries/blob/master/LICENSE.md)
|
|
74
|
+
[](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" />
|