openseries 2.1.4__tar.gz → 2.1.6__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.4 → openseries-2.1.6}/PKG-INFO +3 -3
- {openseries-2.1.4 → openseries-2.1.6}/README.md +2 -2
- {openseries-2.1.4 → openseries-2.1.6}/openseries/__init__.py +2 -0
- {openseries-2.1.4 → openseries-2.1.6}/openseries/_common_model.py +109 -110
- {openseries-2.1.4 → openseries-2.1.6}/openseries/datefixer.py +28 -6
- openseries-2.1.6/openseries/html_utils.py +314 -0
- {openseries-2.1.4 → openseries-2.1.6}/openseries/owntypes.py +4 -3
- {openseries-2.1.4 → openseries-2.1.6}/openseries/plotly_layouts.json +11 -7
- {openseries-2.1.4 → openseries-2.1.6}/openseries/portfoliotools.py +6 -4
- {openseries-2.1.4 → openseries-2.1.6}/openseries/report.py +46 -45
- {openseries-2.1.4 → openseries-2.1.6}/openseries/series.py +79 -2
- {openseries-2.1.4 → openseries-2.1.6}/pyproject.toml +3 -3
- {openseries-2.1.4 → openseries-2.1.6}/LICENSE.md +0 -0
- {openseries-2.1.4 → openseries-2.1.6}/openseries/_risk.py +0 -0
- {openseries-2.1.4 → openseries-2.1.6}/openseries/frame.py +0 -0
- {openseries-2.1.4 → openseries-2.1.6}/openseries/load_plotly.py +0 -0
- {openseries-2.1.4 → openseries-2.1.6}/openseries/plotly_captor_logo.json +0 -0
- {openseries-2.1.4 → openseries-2.1.6}/openseries/py.typed +0 -0
- {openseries-2.1.4 → openseries-2.1.6}/openseries/simulation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openseries
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.6
|
|
4
4
|
Summary: Tools for analyzing financial timeseries.
|
|
5
5
|
License-Expression: BSD-3-Clause
|
|
6
6
|
License-File: LICENSE.md
|
|
@@ -50,7 +50,7 @@ Description-Content-Type: text/markdown
|
|
|
50
50
|
[](https://www.python.org/)
|
|
51
51
|
[](https://github.com/CaptorAB/openseries/actions/workflows/test.yml)
|
|
52
52
|
[](https://codecov.io/gh/CaptorAB/openseries/branch/master)
|
|
53
|
-

|
|
53
|
+
[](https://captorab.github.io/openseries/)
|
|
54
54
|
[](https://python-poetry.org/)
|
|
55
55
|
[](https://beta.ruff.rs/docs/)
|
|
56
56
|
[](https://github.com/CaptorAB/openseries/blob/master/LICENSE.md)
|
|
@@ -60,7 +60,7 @@ Tools for analyzing financial timeseries of a single asset or a group of assets.
|
|
|
60
60
|
|
|
61
61
|
## Documentation
|
|
62
62
|
|
|
63
|
-
Complete documentation is available at: [https://
|
|
63
|
+
Complete documentation is available at: [https://captorab.github.io/openseries/](https://captorab.github.io/openseries/)
|
|
64
64
|
|
|
65
65
|
The documentation includes:
|
|
66
66
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[](https://www.python.org/)
|
|
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
14
|
[](https://python-poetry.org/)
|
|
15
15
|
[](https://beta.ruff.rs/docs/)
|
|
16
16
|
[](https://github.com/CaptorAB/openseries/blob/master/LICENSE.md)
|
|
@@ -20,7 +20,7 @@ Tools for analyzing financial timeseries of a single asset or a group of assets.
|
|
|
20
20
|
|
|
21
21
|
## Documentation
|
|
22
22
|
|
|
23
|
-
Complete documentation is available at: [https://
|
|
23
|
+
Complete documentation is available at: [https://captorab.github.io/openseries/](https://captorab.github.io/openseries/)
|
|
24
24
|
|
|
25
25
|
The documentation includes:
|
|
26
26
|
|
|
@@ -11,6 +11,7 @@ from .datefixer import (
|
|
|
11
11
|
offset_business_days,
|
|
12
12
|
)
|
|
13
13
|
from .frame import OpenFrame
|
|
14
|
+
from .html_utils import export_plotly_figure
|
|
14
15
|
from .load_plotly import load_plotly_dict
|
|
15
16
|
from .owntypes import ValueType
|
|
16
17
|
from .portfoliotools import (
|
|
@@ -33,6 +34,7 @@ __all__ = [
|
|
|
33
34
|
"date_fix",
|
|
34
35
|
"date_offset_foll",
|
|
35
36
|
"efficient_frontier",
|
|
37
|
+
"export_plotly_figure",
|
|
36
38
|
"generate_calendar_date_range",
|
|
37
39
|
"get_previous_business_day_before_today",
|
|
38
40
|
"holiday_calendar",
|
|
@@ -21,7 +21,6 @@ from .owntypes import (
|
|
|
21
21
|
DateAlignmentError,
|
|
22
22
|
InitialValueZeroError,
|
|
23
23
|
NumberOfItemsAndLabelsNotSameError,
|
|
24
|
-
PlotlyConfigType,
|
|
25
24
|
ResampleDataLossError,
|
|
26
25
|
SeriesOrFloat_co,
|
|
27
26
|
ValueType,
|
|
@@ -61,9 +60,7 @@ from pandas import (
|
|
|
61
60
|
from pandas.tseries.offsets import CustomBusinessDay
|
|
62
61
|
from plotly.figure_factory import create_distplot # type: ignore[import-untyped]
|
|
63
62
|
from plotly.graph_objs import Figure # type: ignore[import-untyped]
|
|
64
|
-
from
|
|
65
|
-
from plotly.offline import plot # type: ignore[import-untyped]
|
|
66
|
-
from pydantic import BaseModel, ConfigDict, DirectoryPath, ValidationError
|
|
63
|
+
from pydantic import BaseModel, ConfigDict, DirectoryPath
|
|
67
64
|
from scipy.stats import (
|
|
68
65
|
kurtosis,
|
|
69
66
|
norm,
|
|
@@ -79,6 +76,7 @@ from .datefixer import (
|
|
|
79
76
|
date_offset_foll,
|
|
80
77
|
holiday_calendar,
|
|
81
78
|
)
|
|
79
|
+
from .html_utils import export_plotly_figure
|
|
82
80
|
from .load_plotly import load_plotly_dict
|
|
83
81
|
|
|
84
82
|
|
|
@@ -284,8 +282,9 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
284
282
|
result = (
|
|
285
283
|
self.tsdf.groupby(years)
|
|
286
284
|
.apply(
|
|
287
|
-
lambda prices: (
|
|
288
|
-
|
|
285
|
+
lambda prices: (
|
|
286
|
+
(prices / prices.expanding(min_periods=1).max()).min() - 1
|
|
287
|
+
),
|
|
289
288
|
)
|
|
290
289
|
.min()
|
|
291
290
|
)
|
|
@@ -506,17 +505,19 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
506
505
|
"""
|
|
507
506
|
mdddf = self.tsdf.copy()
|
|
508
507
|
mdddf.index = DatetimeIndex(mdddf.index)
|
|
509
|
-
|
|
508
|
+
idxmin_result = cast(
|
|
509
|
+
"Series[Timestamp]",
|
|
510
|
+
(mdddf / mdddf.expanding(min_periods=1).max()).idxmin(),
|
|
511
|
+
)
|
|
512
|
+
result = idxmin_result.dt.date
|
|
510
513
|
|
|
511
514
|
if self.tsdf.shape[1] == 1:
|
|
512
|
-
return
|
|
513
|
-
|
|
515
|
+
return result.iloc[0]
|
|
516
|
+
return Series(
|
|
514
517
|
data=result,
|
|
515
518
|
index=self.tsdf.columns,
|
|
516
519
|
name="Max drawdown date",
|
|
517
|
-
|
|
518
|
-
).dt.date # type: ignore[attr-defined]
|
|
519
|
-
return cast("Series[dt.date]", date_series)
|
|
520
|
+
)
|
|
520
521
|
|
|
521
522
|
@property
|
|
522
523
|
def worst(self: Self) -> SeriesOrFloat_co:
|
|
@@ -547,12 +548,12 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
547
548
|
"""
|
|
548
549
|
method: LiteralPandasReindexMethod = "nearest"
|
|
549
550
|
|
|
550
|
-
|
|
551
|
+
if hasattr(self, "constituents"):
|
|
552
|
+
countries = self.constituents[0].countries
|
|
553
|
+
markets = self.constituents[0].markets
|
|
554
|
+
else:
|
|
551
555
|
countries = self.countries
|
|
552
556
|
markets = self.markets
|
|
553
|
-
except AttributeError:
|
|
554
|
-
countries = self.constituents[0].countries # type: ignore[attr-defined]
|
|
555
|
-
markets = self.constituents[0].markets # type: ignore[attr-defined]
|
|
556
557
|
|
|
557
558
|
wmdf = self.tsdf.copy()
|
|
558
559
|
|
|
@@ -733,6 +734,54 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
733
734
|
|
|
734
735
|
return earlier, later
|
|
735
736
|
|
|
737
|
+
def _get_or_set_countries(
|
|
738
|
+
self: Self, countries: CountriesType | None
|
|
739
|
+
) -> CountriesType | None:
|
|
740
|
+
"""Get or set countries attribute, handling both OpenTimeSeries and OpenFrame.
|
|
741
|
+
|
|
742
|
+
Args:
|
|
743
|
+
countries: Country code(s) to set, or None to get existing value.
|
|
744
|
+
|
|
745
|
+
Returns:
|
|
746
|
+
The countries value after getting or setting.
|
|
747
|
+
"""
|
|
748
|
+
if countries:
|
|
749
|
+
if hasattr(self, "countries"):
|
|
750
|
+
self.countries = countries
|
|
751
|
+
else:
|
|
752
|
+
for serie in self.constituents: # type: ignore[attr-defined]
|
|
753
|
+
serie.countries = countries
|
|
754
|
+
elif hasattr(self, "countries"):
|
|
755
|
+
countries = self.countries
|
|
756
|
+
else:
|
|
757
|
+
countries = self.constituents[0].countries # type: ignore[attr-defined]
|
|
758
|
+
|
|
759
|
+
return countries
|
|
760
|
+
|
|
761
|
+
def _get_or_set_markets(
|
|
762
|
+
self: Self, markets: list[str] | str | None
|
|
763
|
+
) -> list[str] | str | None:
|
|
764
|
+
"""Get or set markets attribute, handling both OpenTimeSeries and OpenFrame.
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
markets: Market code(s) to set, or None to get existing value.
|
|
768
|
+
|
|
769
|
+
Returns:
|
|
770
|
+
The markets value after getting or setting.
|
|
771
|
+
"""
|
|
772
|
+
if markets:
|
|
773
|
+
if hasattr(self, "markets"):
|
|
774
|
+
self.markets = markets
|
|
775
|
+
else:
|
|
776
|
+
for serie in self.constituents: # type: ignore[attr-defined]
|
|
777
|
+
serie.markets = markets
|
|
778
|
+
elif hasattr(self, "markets"):
|
|
779
|
+
markets = self.markets
|
|
780
|
+
else:
|
|
781
|
+
markets = self.constituents[0].markets # type: ignore[attr-defined]
|
|
782
|
+
|
|
783
|
+
return markets
|
|
784
|
+
|
|
736
785
|
def align_index_to_local_cdays(
|
|
737
786
|
self: Self,
|
|
738
787
|
countries: CountriesType | None = None,
|
|
@@ -754,29 +803,8 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
754
803
|
startyear = cast("int", to_datetime(self.tsdf.index[0]).year)
|
|
755
804
|
endyear = cast("int", to_datetime(self.tsdf.index[-1]).year)
|
|
756
805
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
self.countries = countries
|
|
760
|
-
except ValidationError:
|
|
761
|
-
for serie in self.constituents: # type: ignore[attr-defined]
|
|
762
|
-
serie.countries = countries
|
|
763
|
-
else:
|
|
764
|
-
try:
|
|
765
|
-
countries = self.countries
|
|
766
|
-
except AttributeError:
|
|
767
|
-
countries = self.constituents[0].countries # type: ignore[attr-defined]
|
|
768
|
-
|
|
769
|
-
if markets:
|
|
770
|
-
try:
|
|
771
|
-
self.markets = markets
|
|
772
|
-
except ValidationError:
|
|
773
|
-
for serie in self.constituents: # type: ignore[attr-defined]
|
|
774
|
-
serie.markets = markets
|
|
775
|
-
else:
|
|
776
|
-
try:
|
|
777
|
-
markets = self.markets
|
|
778
|
-
except AttributeError:
|
|
779
|
-
markets = self.constituents[0].markets # type: ignore[attr-defined]
|
|
806
|
+
countries = self._get_or_set_countries(countries)
|
|
807
|
+
markets = self._get_or_set_markets(markets)
|
|
780
808
|
|
|
781
809
|
calendar = holiday_calendar(
|
|
782
810
|
startyear=startyear,
|
|
@@ -1046,76 +1074,44 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
1046
1074
|
def _apply_title_logo(
|
|
1047
1075
|
figure: Figure,
|
|
1048
1076
|
logo: CaptorLogoType,
|
|
1049
|
-
title: str | None,
|
|
1050
1077
|
*,
|
|
1051
1078
|
add_logo: bool,
|
|
1052
|
-
) -> None:
|
|
1053
|
-
"""Apply optional
|
|
1079
|
+
) -> str | None:
|
|
1080
|
+
"""Apply optional logo to a Plotly Figure.
|
|
1054
1081
|
|
|
1055
1082
|
Args:
|
|
1056
1083
|
figure: Plotly figure to update.
|
|
1057
1084
|
logo: Plotly layout image dict.
|
|
1058
|
-
title: Optional plot title.
|
|
1059
1085
|
add_logo: Whether to add the logo to the figure.
|
|
1086
|
+
|
|
1087
|
+
Returns:
|
|
1088
|
+
Logo source URL if logo should be displayed, None otherwise.
|
|
1060
1089
|
"""
|
|
1090
|
+
logo_url: str | None = None
|
|
1061
1091
|
if add_logo:
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
figure.
|
|
1065
|
-
{
|
|
1092
|
+
source = logo.get("source", "")
|
|
1093
|
+
logo_url = str(source) if source else None
|
|
1094
|
+
figure.add_layout_image(
|
|
1095
|
+
{
|
|
1096
|
+
"source": "",
|
|
1097
|
+
"x": 0,
|
|
1098
|
+
"y": 1,
|
|
1099
|
+
"xanchor": "left",
|
|
1100
|
+
"yanchor": "top",
|
|
1101
|
+
"xref": "paper",
|
|
1102
|
+
"yref": "paper",
|
|
1103
|
+
"sizex": 0,
|
|
1104
|
+
"sizey": 0,
|
|
1105
|
+
"opacity": 0,
|
|
1106
|
+
}
|
|
1066
1107
|
)
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
output_type: LiteralPlotlyOutput,
|
|
1073
|
-
plotfile: Path,
|
|
1074
|
-
filename: str,
|
|
1075
|
-
*,
|
|
1076
|
-
include_plotlyjs_bool: LiteralPlotlyJSlib,
|
|
1077
|
-
auto_open: bool,
|
|
1078
|
-
) -> str:
|
|
1079
|
-
"""Write a file or return inline HTML string from a Plotly Figure.
|
|
1080
|
-
|
|
1081
|
-
Args:
|
|
1082
|
-
figure: Plotly figure to render.
|
|
1083
|
-
fig_config: Plotly config dict.
|
|
1084
|
-
output_type: Output type: ``"file"`` or ``"div"``.
|
|
1085
|
-
plotfile: Full path to the output html file.
|
|
1086
|
-
filename: Output filename used for the ``div_id`` when inline.
|
|
1087
|
-
include_plotlyjs_bool: How plotly.js is included.
|
|
1088
|
-
auto_open: Whether to auto-open the file in a browser.
|
|
1089
|
-
|
|
1090
|
-
Returns:
|
|
1091
|
-
If ``output_type`` is ``"file"``, the path to the file; otherwise an
|
|
1092
|
-
inline HTML string (div).
|
|
1093
|
-
"""
|
|
1094
|
-
if output_type == "file":
|
|
1095
|
-
plot(
|
|
1096
|
-
figure_or_data=figure,
|
|
1097
|
-
filename=str(plotfile),
|
|
1098
|
-
auto_open=auto_open,
|
|
1099
|
-
auto_play=False,
|
|
1100
|
-
link_text="",
|
|
1101
|
-
include_plotlyjs=include_plotlyjs_bool,
|
|
1102
|
-
config=fig_config,
|
|
1103
|
-
output_type=output_type,
|
|
1104
|
-
)
|
|
1105
|
-
return str(plotfile)
|
|
1106
|
-
|
|
1107
|
-
div_id = filename.rsplit(".", 1)[0]
|
|
1108
|
-
return cast(
|
|
1109
|
-
"str",
|
|
1110
|
-
to_html(
|
|
1111
|
-
fig=figure,
|
|
1112
|
-
config=fig_config,
|
|
1113
|
-
auto_play=False,
|
|
1114
|
-
include_plotlyjs=include_plotlyjs_bool,
|
|
1115
|
-
full_html=False,
|
|
1116
|
-
div_id=div_id,
|
|
1117
|
-
),
|
|
1108
|
+
figure.update_layout(
|
|
1109
|
+
{
|
|
1110
|
+
"margin": {"t": 20, "b": 60, "l": 60, "r": 60, "pad": 4},
|
|
1111
|
+
"autosize": True,
|
|
1112
|
+
},
|
|
1118
1113
|
)
|
|
1114
|
+
return logo_url
|
|
1119
1115
|
|
|
1120
1116
|
def plot_bars(
|
|
1121
1117
|
self: Self,
|
|
@@ -1176,21 +1172,22 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
1176
1172
|
)
|
|
1177
1173
|
figure.update_layout(barmode=mode, yaxis={"tickformat": tick_fmt})
|
|
1178
1174
|
|
|
1179
|
-
self._apply_title_logo(
|
|
1175
|
+
logo_url = self._apply_title_logo(
|
|
1180
1176
|
figure=figure,
|
|
1181
|
-
title=title,
|
|
1182
1177
|
add_logo=add_logo,
|
|
1183
1178
|
logo=logo,
|
|
1184
1179
|
)
|
|
1185
1180
|
|
|
1186
|
-
string_output =
|
|
1181
|
+
string_output = export_plotly_figure(
|
|
1187
1182
|
figure=figure,
|
|
1188
1183
|
fig_config=fig["config"],
|
|
1189
|
-
|
|
1184
|
+
include_plotlyjs=include_plotlyjs,
|
|
1190
1185
|
output_type=output_type,
|
|
1191
1186
|
auto_open=auto_open,
|
|
1192
1187
|
plotfile=plotfile,
|
|
1193
1188
|
filename=filename,
|
|
1189
|
+
title=title,
|
|
1190
|
+
logo_url=logo_url,
|
|
1194
1191
|
)
|
|
1195
1192
|
|
|
1196
1193
|
return figure, string_output
|
|
@@ -1271,21 +1268,22 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
1271
1268
|
textposition="top center",
|
|
1272
1269
|
)
|
|
1273
1270
|
|
|
1274
|
-
self._apply_title_logo(
|
|
1271
|
+
logo_url = self._apply_title_logo(
|
|
1275
1272
|
figure=figure,
|
|
1276
|
-
title=title,
|
|
1277
1273
|
add_logo=add_logo,
|
|
1278
1274
|
logo=logo,
|
|
1279
1275
|
)
|
|
1280
1276
|
|
|
1281
|
-
string_output =
|
|
1277
|
+
string_output = export_plotly_figure(
|
|
1282
1278
|
figure=figure,
|
|
1283
1279
|
fig_config=fig["config"],
|
|
1284
|
-
|
|
1280
|
+
include_plotlyjs=include_plotlyjs,
|
|
1285
1281
|
output_type=output_type,
|
|
1286
1282
|
auto_open=auto_open,
|
|
1287
1283
|
plotfile=plotfile,
|
|
1288
1284
|
filename=filename,
|
|
1285
|
+
title=title,
|
|
1286
|
+
logo_url=logo_url,
|
|
1289
1287
|
)
|
|
1290
1288
|
|
|
1291
1289
|
return figure, string_output
|
|
@@ -1393,21 +1391,22 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
1393
1391
|
figure.update_xaxes(zeroline=True, zerolinewidth=2, zerolinecolor="lightgrey")
|
|
1394
1392
|
figure.update_yaxes(zeroline=True, zerolinewidth=2, zerolinecolor="lightgrey")
|
|
1395
1393
|
|
|
1396
|
-
self._apply_title_logo(
|
|
1394
|
+
logo_url = self._apply_title_logo(
|
|
1397
1395
|
figure=figure,
|
|
1398
|
-
title=title,
|
|
1399
1396
|
add_logo=add_logo,
|
|
1400
1397
|
logo=logo,
|
|
1401
1398
|
)
|
|
1402
1399
|
|
|
1403
|
-
string_output =
|
|
1400
|
+
string_output = export_plotly_figure(
|
|
1404
1401
|
figure=figure,
|
|
1405
1402
|
fig_config=fig_dict["config"],
|
|
1406
|
-
|
|
1403
|
+
include_plotlyjs=include_plotlyjs,
|
|
1407
1404
|
output_type=output_type,
|
|
1408
1405
|
auto_open=auto_open,
|
|
1409
1406
|
plotfile=plotfile,
|
|
1410
1407
|
filename=filename,
|
|
1408
|
+
title=title,
|
|
1409
|
+
logo_url=logo_url,
|
|
1411
1410
|
)
|
|
1412
1411
|
|
|
1413
1412
|
return figure, string_output
|
|
@@ -374,13 +374,22 @@ def generate_calendar_date_range(
|
|
|
374
374
|
raise TradingDaysNotAboveZeroError(msg)
|
|
375
375
|
|
|
376
376
|
if start and not end:
|
|
377
|
+
adjusted_start = date_offset_foll(
|
|
378
|
+
raw_date=start,
|
|
379
|
+
months_offset=0,
|
|
380
|
+
countries=countries,
|
|
381
|
+
markets=markets,
|
|
382
|
+
custom_holidays=custom_holidays,
|
|
383
|
+
adjust=True,
|
|
384
|
+
following=True,
|
|
385
|
+
)
|
|
377
386
|
tmp_range = date_range(
|
|
378
|
-
start=
|
|
387
|
+
start=adjusted_start,
|
|
379
388
|
periods=trading_days * 365 // 252,
|
|
380
389
|
freq="D",
|
|
381
390
|
)
|
|
382
391
|
calendar = holiday_calendar(
|
|
383
|
-
startyear=
|
|
392
|
+
startyear=adjusted_start.year,
|
|
384
393
|
endyear=date_fix(tmp_range.tolist()[-1]).year,
|
|
385
394
|
countries=countries,
|
|
386
395
|
markets=markets,
|
|
@@ -389,17 +398,30 @@ def generate_calendar_date_range(
|
|
|
389
398
|
return [
|
|
390
399
|
d.date()
|
|
391
400
|
for d in date_range(
|
|
392
|
-
start=
|
|
401
|
+
start=adjusted_start,
|
|
393
402
|
periods=trading_days,
|
|
394
403
|
freq=CustomBusinessDay(calendar=calendar),
|
|
395
404
|
)
|
|
396
405
|
]
|
|
397
406
|
|
|
398
407
|
if end and not start:
|
|
399
|
-
|
|
408
|
+
adjusted_end = date_offset_foll(
|
|
409
|
+
raw_date=end,
|
|
410
|
+
months_offset=0,
|
|
411
|
+
countries=countries,
|
|
412
|
+
markets=markets,
|
|
413
|
+
custom_holidays=custom_holidays,
|
|
414
|
+
adjust=True,
|
|
415
|
+
following=False,
|
|
416
|
+
)
|
|
417
|
+
tmp_range = date_range(
|
|
418
|
+
end=adjusted_end,
|
|
419
|
+
periods=trading_days * 365 // 252,
|
|
420
|
+
freq="D",
|
|
421
|
+
)
|
|
400
422
|
calendar = holiday_calendar(
|
|
401
423
|
startyear=date_fix(tmp_range.tolist()[0]).year,
|
|
402
|
-
endyear=
|
|
424
|
+
endyear=adjusted_end.year,
|
|
403
425
|
countries=countries,
|
|
404
426
|
markets=markets,
|
|
405
427
|
custom_holidays=custom_holidays,
|
|
@@ -407,7 +429,7 @@ def generate_calendar_date_range(
|
|
|
407
429
|
return [
|
|
408
430
|
d.date()
|
|
409
431
|
for d in date_range(
|
|
410
|
-
end=
|
|
432
|
+
end=adjusted_end,
|
|
411
433
|
periods=trading_days,
|
|
412
434
|
freq=CustomBusinessDay(calendar=calendar),
|
|
413
435
|
)
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Shared HTML utilities for responsive Plotly outputs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, cast
|
|
7
|
+
from webbrowser import open as webbrowser_open
|
|
8
|
+
|
|
9
|
+
from plotly.io import to_html # type: ignore[import-untyped]
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
12
|
+
from plotly.graph_objs import Figure # type: ignore[import-untyped]
|
|
13
|
+
|
|
14
|
+
from .owntypes import LiteralPlotlyJSlib, LiteralPlotlyOutput, PlotlyConfigType
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"_generate_responsive_plot_html",
|
|
18
|
+
"_get_base_css",
|
|
19
|
+
"_get_plot_css",
|
|
20
|
+
"_get_plotly_script",
|
|
21
|
+
"export_plotly_figure",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_base_css() -> str:
|
|
26
|
+
"""Get base CSS styles for responsive HTML reports."""
|
|
27
|
+
return """
|
|
28
|
+
:root{--ink:#1f2a44;--muted:#6b778c;--header:#4a4a4a;--header2:#6a6a6a;--cell:#f3f3f3;--cell2:#e6e6e6;--paper:#ffffff;}
|
|
29
|
+
html,body{margin:0;padding:0;background:var(--paper);color:var(--ink);
|
|
30
|
+
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;}
|
|
31
|
+
.page{max-width:calc(100% - 64px);margin:0 auto;padding:32px;
|
|
32
|
+
padding-bottom:48px;}
|
|
33
|
+
.header{display:grid;grid-template-columns:140px 1fr 140px;gap:12px;
|
|
34
|
+
align-items:start;}
|
|
35
|
+
h1{margin:0;text-align:center;font-size:45px;font-weight:800;}
|
|
36
|
+
.layout{display:grid;grid-template-columns:1.2fr .9fr;
|
|
37
|
+
grid-template-areas:"charts table";gap:22px;align-items:start;margin-top:12px;}
|
|
38
|
+
.charts{grid-area:charts;display:grid;grid-template-rows:auto auto;gap:18px;}
|
|
39
|
+
.table{grid-area:table;}
|
|
40
|
+
.plot{width:100%;height:380px;}
|
|
41
|
+
.plot.bar{height:300px;}
|
|
42
|
+
@media (max-width:980px){
|
|
43
|
+
.page{padding:24px;padding-bottom:24px;}
|
|
44
|
+
.header{grid-template-columns:120px 1fr;}
|
|
45
|
+
h1{font-size:36px;}
|
|
46
|
+
.layout{grid-template-columns:1fr;grid-template-areas:"table" "charts";gap:16px;}
|
|
47
|
+
.plot{height:380px;}
|
|
48
|
+
.plot.bar{height:300px;}
|
|
49
|
+
.table{overflow-x:auto;}
|
|
50
|
+
table.metrics{table-layout:auto;font-size:14px;}
|
|
51
|
+
}
|
|
52
|
+
table.metrics{width:100%;border-collapse:separate;border-spacing:0;font-size:12px;
|
|
53
|
+
border-radius:4px;overflow:hidden;table-layout:fixed;}
|
|
54
|
+
table.metrics thead th{background:var(--header);color:white;padding:8px 10px;
|
|
55
|
+
font-weight:700;text-align:center;word-wrap:break-word;word-break:break-word;}
|
|
56
|
+
table.metrics thead th:first-child{background:var(--header2);text-align:left;
|
|
57
|
+
width:180px;}
|
|
58
|
+
table.metrics tbody td{padding:7px 10px;border-bottom:1px solid white;
|
|
59
|
+
border-right:1px solid white;text-align:center;background:var(--paper);}
|
|
60
|
+
table.metrics tbody td:first-child{text-align:left;font-weight:600;color:white;
|
|
61
|
+
background:var(--header);width:180px;}
|
|
62
|
+
table.metrics tbody td:last-child{background:var(--cell2);}
|
|
63
|
+
.legend-container{margin-top:24px;padding-top:20px;padding-bottom:16px;
|
|
64
|
+
display:flex;justify-content:center;flex-wrap:wrap;gap:24px;flex-shrink:0;}
|
|
65
|
+
.legend-item{display:flex;align-items:center;gap:8px;font-size:14px;}
|
|
66
|
+
.legend-color{width:20px;height:3px;border-radius:2px;}
|
|
67
|
+
@media (min-width:981px){
|
|
68
|
+
html,body{overflow-y:auto;}
|
|
69
|
+
}
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _get_plot_css() -> str:
|
|
74
|
+
"""Get CSS styles for full-screen responsive plots.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
CSS string for responsive plots.
|
|
78
|
+
"""
|
|
79
|
+
return """
|
|
80
|
+
*{box-sizing:border-box;}
|
|
81
|
+
html,body{margin:0;padding:0;width:100%;height:100%;
|
|
82
|
+
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;
|
|
83
|
+
overflow-x:hidden;}
|
|
84
|
+
.page{width:100%;height:100vh;display:flex;flex-direction:column;}
|
|
85
|
+
.title-container{width:100%;flex-shrink:0;padding:15px 20px 10px 20px;
|
|
86
|
+
display:flex;align-items:center;justify-content:center;background:white;z-index:10;
|
|
87
|
+
gap:15px;}
|
|
88
|
+
.title-logo{height:60px;width:auto;flex-shrink:0;}
|
|
89
|
+
.title-container h1{margin:0;padding:0;font-size:36px;font-weight:bold;
|
|
90
|
+
color:#253551;word-wrap:break-word;word-break:break-word;
|
|
91
|
+
white-space:normal;line-height:1.2;flex:1;text-align:center;}
|
|
92
|
+
.plot-container{width:100%;flex:1;position:relative;overflow:hidden;
|
|
93
|
+
min-height:0;}
|
|
94
|
+
.plot{width:100%;height:100%;display:block;position:absolute;top:0;left:0;}
|
|
95
|
+
.plot > div{width:100% !important;height:100% !important;}
|
|
96
|
+
.plot .js-plotly-plot{width:100% !important;height:100% !important;}
|
|
97
|
+
.plot .js-plotly-plot .plotly{width:100% !important;height:100% !important;}
|
|
98
|
+
@media (max-width: 980px) {
|
|
99
|
+
.title-container{padding:12px 15px 8px 15px;gap:12px;}
|
|
100
|
+
.title-container h1{font-size:24px;line-height:1.3;}
|
|
101
|
+
.title-logo{height:32px;}
|
|
102
|
+
}
|
|
103
|
+
@media (max-width: 480px) {
|
|
104
|
+
.title-container{padding:10px 12px 6px 12px;gap:10px;}
|
|
105
|
+
.title-container h1{font-size:18px;line-height:1.2;}
|
|
106
|
+
.title-logo{height:24px;}
|
|
107
|
+
}
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _get_plotly_script(include_plotlyjs: LiteralPlotlyJSlib) -> str:
|
|
112
|
+
"""Get plotly script tag."""
|
|
113
|
+
if include_plotlyjs == "cdn":
|
|
114
|
+
return '<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>'
|
|
115
|
+
return ""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _generate_responsive_plot_html(
|
|
119
|
+
title: str | None,
|
|
120
|
+
plot_div: str,
|
|
121
|
+
include_plotlyjs: LiteralPlotlyJSlib,
|
|
122
|
+
plot_id: str,
|
|
123
|
+
logo_url: str | None = None,
|
|
124
|
+
) -> str:
|
|
125
|
+
"""Generate responsive HTML for a single Plotly plot."""
|
|
126
|
+
css = _get_plot_css()
|
|
127
|
+
plotly_script = _get_plotly_script(include_plotlyjs)
|
|
128
|
+
|
|
129
|
+
plot_div = plot_div.replace(
|
|
130
|
+
f'<div id="{plot_id}"', f'<div id="{plot_id}" class="plot"'
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
title_html = ""
|
|
134
|
+
if (title is not None and title) or logo_url:
|
|
135
|
+
logo_html = ""
|
|
136
|
+
if logo_url:
|
|
137
|
+
logo_html = f'<img src="{logo_url}" alt="Logo" class="title-logo" />'
|
|
138
|
+
title_text = f"<h1><b>{title}</b></h1>" if title else ""
|
|
139
|
+
title_html = f'<div class="title-container">{logo_html}{title_text}</div>'
|
|
140
|
+
|
|
141
|
+
return f"""<!doctype html>
|
|
142
|
+
<html lang="en">
|
|
143
|
+
<head>
|
|
144
|
+
<meta charset="utf-8" />
|
|
145
|
+
<meta name="viewport" content="width=device-width,initial-scale=1,
|
|
146
|
+
maximum-scale=5,user-scalable=yes" />
|
|
147
|
+
<title>{title or ""}</title>
|
|
148
|
+
<style>{css}</style>
|
|
149
|
+
{plotly_script}
|
|
150
|
+
</head>
|
|
151
|
+
<body>
|
|
152
|
+
<div class="page">
|
|
153
|
+
{title_html}
|
|
154
|
+
<div class="plot-container">
|
|
155
|
+
{plot_div}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
<script>
|
|
159
|
+
(function() {{
|
|
160
|
+
var plotDiv = document.getElementById("{plot_id}");
|
|
161
|
+
var container = plotDiv ? plotDiv.closest('.plot-container') : null;
|
|
162
|
+
|
|
163
|
+
function getContainerSize() {{
|
|
164
|
+
if (!container) return {{width: window.innerWidth, height: window.innerHeight}};
|
|
165
|
+
var rect = container.getBoundingClientRect();
|
|
166
|
+
return {{width: rect.width, height: rect.height}};
|
|
167
|
+
}}
|
|
168
|
+
|
|
169
|
+
function resizePlot() {{
|
|
170
|
+
if (typeof Plotly === 'undefined' || !Plotly.Plots || !plotDiv) return;
|
|
171
|
+
|
|
172
|
+
var size = getContainerSize();
|
|
173
|
+
if (size.width > 0 && size.height > 0) {{
|
|
174
|
+
Plotly.Plots.resize(plotDiv);
|
|
175
|
+
}}
|
|
176
|
+
}}
|
|
177
|
+
|
|
178
|
+
function debounceResize() {{
|
|
179
|
+
clearTimeout(window._resizeTimeout);
|
|
180
|
+
window._resizeTimeout = setTimeout(resizePlot, 150);
|
|
181
|
+
}}
|
|
182
|
+
|
|
183
|
+
window.addEventListener("resize", debounceResize);
|
|
184
|
+
|
|
185
|
+
window.addEventListener("orientationchange", function() {{
|
|
186
|
+
setTimeout(function() {{
|
|
187
|
+
resizePlot();
|
|
188
|
+
}}, 300);
|
|
189
|
+
}});
|
|
190
|
+
|
|
191
|
+
function initResize() {{
|
|
192
|
+
if (typeof Plotly !== 'undefined' && plotDiv) {{
|
|
193
|
+
setTimeout(resizePlot, 100);
|
|
194
|
+
setTimeout(resizePlot, 500);
|
|
195
|
+
setTimeout(resizePlot, 1000);
|
|
196
|
+
}} else {{
|
|
197
|
+
setTimeout(initResize, 100);
|
|
198
|
+
}}
|
|
199
|
+
}}
|
|
200
|
+
|
|
201
|
+
if (document.readyState === 'loading') {{
|
|
202
|
+
document.addEventListener('DOMContentLoaded', initResize);
|
|
203
|
+
}} else {{
|
|
204
|
+
initResize();
|
|
205
|
+
}}
|
|
206
|
+
|
|
207
|
+
window.addEventListener('load', function() {{
|
|
208
|
+
setTimeout(resizePlot, 200);
|
|
209
|
+
}});
|
|
210
|
+
}})();
|
|
211
|
+
</script>
|
|
212
|
+
</body>
|
|
213
|
+
</html>
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def export_plotly_figure(
|
|
218
|
+
figure: Figure,
|
|
219
|
+
fig_config: PlotlyConfigType,
|
|
220
|
+
output_type: LiteralPlotlyOutput,
|
|
221
|
+
filename: str,
|
|
222
|
+
*,
|
|
223
|
+
include_plotlyjs: LiteralPlotlyJSlib,
|
|
224
|
+
auto_open: bool = False,
|
|
225
|
+
plotfile: Path | str,
|
|
226
|
+
title: str | None = None,
|
|
227
|
+
logo_url: str | None = None,
|
|
228
|
+
) -> str:
|
|
229
|
+
"""Export a Plotly figure to a mobile-responsive HTML file or inline div.
|
|
230
|
+
|
|
231
|
+
This function converts a Plotly figure to HTML output, with support for
|
|
232
|
+
mobile-responsive standalone HTML files or inline HTML divs. When exporting
|
|
233
|
+
to a file, the output includes proper viewport settings, responsive CSS,
|
|
234
|
+
and JavaScript for handling window resizing and orientation changes.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
figure: Plotly figure to render.
|
|
238
|
+
fig_config: Plotly config dictionary.
|
|
239
|
+
output_type: Output type: ``"file"`` or ``"div"``.
|
|
240
|
+
filename: Output filename used for the ``div_id`` when inline, or to
|
|
241
|
+
derive the div_id when exporting to file.
|
|
242
|
+
include_plotlyjs: How plotly.js is included. Can be ``True`` (inline),
|
|
243
|
+
``False`` (not included), or ``"cdn"`` (CDN link).
|
|
244
|
+
auto_open: Whether to auto-open the file in a browser (only used when
|
|
245
|
+
``output_type="file"``). Defaults to ``False``.
|
|
246
|
+
plotfile: Full path to the output HTML file. Required when
|
|
247
|
+
``output_type="file"``, ignored when ``output_type="div"``.
|
|
248
|
+
title: Title for the HTML page (used for file output). Defaults to
|
|
249
|
+
``None``.
|
|
250
|
+
logo_url: Optional logo URL to display in the title container.
|
|
251
|
+
Defaults to ``None``.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
If ``output_type`` is ``"file"``, the path to the file as a string;
|
|
255
|
+
otherwise an inline HTML string (div).
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
Example:
|
|
259
|
+
Export a Plotly figure to a responsive HTML file:
|
|
260
|
+
|
|
261
|
+
>>> import plotly.graph_objects as go
|
|
262
|
+
>>> from openseries.html_utils import export_plotly_figure
|
|
263
|
+
>>> from pathlib import Path
|
|
264
|
+
>>>
|
|
265
|
+
>>> fig = go.Figure(data=go.Scatter(x=[1, 2, 3], y=[4, 5, 6]))
|
|
266
|
+
>>> output_path = export_plotly_figure(
|
|
267
|
+
... figure=fig,
|
|
268
|
+
... fig_config={},
|
|
269
|
+
... output_type="file",
|
|
270
|
+
... filename="my_plot.html",
|
|
271
|
+
... include_plotlyjs="cdn",
|
|
272
|
+
... plotfile=Path("output/my_plot.html"),
|
|
273
|
+
... title="My Plot",
|
|
274
|
+
... auto_open=True,
|
|
275
|
+
... )
|
|
276
|
+
"""
|
|
277
|
+
if output_type == "file":
|
|
278
|
+
plotfile_path = Path(plotfile) if isinstance(plotfile, str) else plotfile
|
|
279
|
+
div_id = filename.rsplit(".", 1)[0]
|
|
280
|
+
plot_div = cast(
|
|
281
|
+
"str",
|
|
282
|
+
to_html(
|
|
283
|
+
fig=figure,
|
|
284
|
+
config=fig_config,
|
|
285
|
+
auto_play=False,
|
|
286
|
+
include_plotlyjs=False,
|
|
287
|
+
full_html=False,
|
|
288
|
+
div_id=div_id,
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
html_content = _generate_responsive_plot_html(
|
|
292
|
+
title=title,
|
|
293
|
+
plot_div=plot_div,
|
|
294
|
+
include_plotlyjs=include_plotlyjs,
|
|
295
|
+
plot_id=div_id,
|
|
296
|
+
logo_url=logo_url,
|
|
297
|
+
)
|
|
298
|
+
plotfile_path.write_text(html_content, encoding="utf-8")
|
|
299
|
+
if auto_open:
|
|
300
|
+
webbrowser_open(plotfile_path.as_uri())
|
|
301
|
+
return str(plotfile_path)
|
|
302
|
+
|
|
303
|
+
div_id = filename.rsplit(".", 1)[0]
|
|
304
|
+
return cast(
|
|
305
|
+
"str",
|
|
306
|
+
to_html(
|
|
307
|
+
fig=figure,
|
|
308
|
+
config=fig_config,
|
|
309
|
+
auto_play=False,
|
|
310
|
+
include_plotlyjs=include_plotlyjs,
|
|
311
|
+
full_html=False,
|
|
312
|
+
div_id=div_id,
|
|
313
|
+
),
|
|
314
|
+
)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import datetime as dt
|
|
6
|
-
from enum import
|
|
6
|
+
from enum import StrEnum
|
|
7
7
|
from pprint import pformat
|
|
8
8
|
from typing import (
|
|
9
9
|
TYPE_CHECKING,
|
|
@@ -300,10 +300,11 @@ class OpenFramePropertiesList(PropertiesList):
|
|
|
300
300
|
self._validate()
|
|
301
301
|
|
|
302
302
|
|
|
303
|
-
class ValueType(
|
|
303
|
+
class ValueType(StrEnum):
|
|
304
304
|
"""Enum types of OpenTimeSeries to identify the output."""
|
|
305
305
|
|
|
306
|
-
|
|
306
|
+
EWMA_VOL = "EWMA volatility"
|
|
307
|
+
EWMA_VAR = "EWMA VaR"
|
|
307
308
|
PRICE = "Price(Close)"
|
|
308
309
|
RTRN = "Return(Total)"
|
|
309
310
|
RELRTRN = "Relative return"
|
|
@@ -41,22 +41,26 @@
|
|
|
41
41
|
"legend": {
|
|
42
42
|
"bgcolor": "rgba(0,0,0,0)",
|
|
43
43
|
"orientation": "h",
|
|
44
|
-
"x": 0.
|
|
45
|
-
"xanchor": "
|
|
46
|
-
"
|
|
47
|
-
"
|
|
44
|
+
"x": 0.5,
|
|
45
|
+
"xanchor": "center",
|
|
46
|
+
"xref": "container",
|
|
47
|
+
"y": 1.05,
|
|
48
|
+
"yanchor": "bottom",
|
|
49
|
+
"yref": "container"
|
|
48
50
|
},
|
|
49
51
|
"paper_bgcolor": "rgba(255,255,255,1)",
|
|
50
52
|
"plot_bgcolor": "rgba(255,255,255,1)",
|
|
51
53
|
"showlegend": true,
|
|
52
54
|
"title": {
|
|
53
55
|
"font": {
|
|
54
|
-
"size":
|
|
56
|
+
"size": 36
|
|
55
57
|
},
|
|
56
58
|
"x": 0.5,
|
|
57
59
|
"xanchor": "center",
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
+
"xref": "container",
|
|
61
|
+
"y": 0.93,
|
|
62
|
+
"yanchor": "bottom",
|
|
63
|
+
"yref": "container"
|
|
60
64
|
},
|
|
61
65
|
"xaxis": {
|
|
62
66
|
"gridcolor": "#EEEEEE",
|
|
@@ -296,8 +296,10 @@ def _build_frontier_dataframe(
|
|
|
296
296
|
weight_cols = columns_lvl_zero
|
|
297
297
|
weight_header = "<br><br>Weights:<br>"
|
|
298
298
|
line_df["text"] = line_df[weight_cols].apply(
|
|
299
|
-
lambda row:
|
|
300
|
-
|
|
299
|
+
lambda row: (
|
|
300
|
+
weight_header
|
|
301
|
+
+ "<br>".join([f"{row[col]:.1%} {col}" for col in weight_cols])
|
|
302
|
+
),
|
|
301
303
|
axis=1,
|
|
302
304
|
)
|
|
303
305
|
|
|
@@ -738,7 +740,7 @@ def _configure_figure_layout(
|
|
|
738
740
|
if title:
|
|
739
741
|
if titletext is None:
|
|
740
742
|
titletext = "<b>Risk and Return</b><br>"
|
|
741
|
-
figure.update_layout(title={"text": titletext, "font": {"size":
|
|
743
|
+
figure.update_layout(title={"text": titletext, "font": {"size": 36}})
|
|
742
744
|
|
|
743
745
|
if add_logo:
|
|
744
746
|
figure.add_layout_image(logo)
|
|
@@ -781,7 +783,7 @@ def _generate_sharpeplot_output(
|
|
|
781
783
|
)
|
|
782
784
|
return str(plotfile)
|
|
783
785
|
|
|
784
|
-
div_id = filename.split(sep=".")[0]
|
|
786
|
+
div_id = filename.split(maxsplit=1, sep=".")[0]
|
|
785
787
|
return cast(
|
|
786
788
|
"str",
|
|
787
789
|
to_html(
|
|
@@ -17,6 +17,7 @@ from pandas import DataFrame, Index, Series, Timestamp, concat, isna
|
|
|
17
17
|
from plotly.graph_objs import Bar, Figure, Scatter # type: ignore[import-untyped]
|
|
18
18
|
from plotly.utils import PlotlyJSONEncoder # type: ignore[import-untyped]
|
|
19
19
|
|
|
20
|
+
from .html_utils import _get_base_css, _get_plotly_script
|
|
20
21
|
from .load_plotly import load_plotly_dict
|
|
21
22
|
from .owntypes import (
|
|
22
23
|
CaptorLogoType,
|
|
@@ -408,12 +409,10 @@ def _get_legend_html(line_traces: list[Scatter], colorway: list[str]) -> str:
|
|
|
408
409
|
|
|
409
410
|
def _get_css() -> str:
|
|
410
411
|
"""Get CSS styles for the HTML report."""
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
.page{max-width:calc(100% - 64px);margin:0 auto;padding:32px;
|
|
416
|
-
padding-bottom:48px;}
|
|
412
|
+
base_css = _get_base_css()
|
|
413
|
+
return (
|
|
414
|
+
base_css
|
|
415
|
+
+ """
|
|
417
416
|
.header{display:grid;grid-template-columns:140px 1fr 140px;gap:12px;
|
|
418
417
|
align-items:start;}
|
|
419
418
|
h1{margin:0;text-align:center;font-size:45px;font-weight:800;}
|
|
@@ -430,16 +429,22 @@ def _get_css() -> str:
|
|
|
430
429
|
.layout{grid-template-columns:1fr;grid-template-areas:"table" "charts";gap:16px;}
|
|
431
430
|
.plot{height:380px;}
|
|
432
431
|
.plot.bar{height:300px;}
|
|
432
|
+
table.metrics{table-layout:fixed;width:auto;}
|
|
433
|
+
table.metrics thead th{min-width:120px;width:120px;white-space:nowrap;}
|
|
434
|
+
table.metrics thead th:first-child{width:180px;}
|
|
435
|
+
table.metrics tbody td{min-width:120px;width:120px;}
|
|
436
|
+
table.metrics tbody td:first-child{width:180px;}
|
|
433
437
|
}
|
|
434
438
|
table.metrics{width:100%;border-collapse:separate;border-spacing:0;font-size:12px;
|
|
435
|
-
border-radius:4px;overflow:hidden;}
|
|
439
|
+
border-radius:4px;overflow:hidden;table-layout:fixed;}
|
|
436
440
|
table.metrics thead th{background:var(--header);color:white;padding:8px 10px;
|
|
437
|
-
font-weight:700;text-align:center;
|
|
438
|
-
table.metrics thead th:first-child{background:var(--header2);text-align:left;
|
|
441
|
+
font-weight:700;text-align:center;word-wrap:break-word;word-break:break-word;}
|
|
442
|
+
table.metrics thead th:first-child{background:var(--header2);text-align:left;
|
|
443
|
+
width:180px;}
|
|
439
444
|
table.metrics tbody td{padding:7px 10px;border-bottom:1px solid white;
|
|
440
445
|
border-right:1px solid white;text-align:center;background:var(--paper);}
|
|
441
446
|
table.metrics tbody td:first-child{text-align:left;font-weight:600;color:white;
|
|
442
|
-
background:var(--header);width:
|
|
447
|
+
background:var(--header);width:180px;}
|
|
443
448
|
table.metrics tbody td:last-child{background:var(--cell2);}
|
|
444
449
|
.legend-container{margin-top:24px;padding-top:20px;padding-bottom:16px;
|
|
445
450
|
display:flex;justify-content:center;flex-wrap:wrap;gap:24px;flex-shrink:0;}
|
|
@@ -449,13 +454,7 @@ def _get_css() -> str:
|
|
|
449
454
|
html,body{overflow-y:auto;}
|
|
450
455
|
}
|
|
451
456
|
"""
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
def _get_plotly_script(include_plotlyjs: LiteralPlotlyJSlib) -> str:
|
|
455
|
-
"""Get plotly script tag."""
|
|
456
|
-
if include_plotlyjs == "cdn":
|
|
457
|
-
return '<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>'
|
|
458
|
-
return ""
|
|
457
|
+
)
|
|
459
458
|
|
|
460
459
|
|
|
461
460
|
def _write_html_file(
|
|
@@ -587,19 +586,21 @@ def report_html(
|
|
|
587
586
|
|
|
588
587
|
for item, f in zip(rpt_df.index, formats, strict=False):
|
|
589
588
|
rpt_df.loc[item] = rpt_df.loc[item].apply(
|
|
590
|
-
lambda x, fmt=f:
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
str(x)
|
|
598
|
-
if isinstance(x, str)
|
|
589
|
+
lambda x, fmt=f: (
|
|
590
|
+
""
|
|
591
|
+
if (
|
|
592
|
+
x is None
|
|
593
|
+
or (not isinstance(x, str) and isna(x))
|
|
594
|
+
or (isinstance(x, str) and x.lower() in ("nan", "nan%", ""))
|
|
595
|
+
)
|
|
599
596
|
else (
|
|
600
|
-
|
|
601
|
-
if
|
|
602
|
-
else
|
|
597
|
+
str(x)
|
|
598
|
+
if isinstance(x, str)
|
|
599
|
+
else (
|
|
600
|
+
Timestamp(x).strftime("%Y-%m-%d")
|
|
601
|
+
if "%Y-%m-%d" in fmt and not isinstance(x, str)
|
|
602
|
+
else fmt.format(x)
|
|
603
|
+
)
|
|
603
604
|
)
|
|
604
605
|
),
|
|
605
606
|
)
|
|
@@ -611,7 +612,7 @@ def report_html(
|
|
|
611
612
|
rpt_df.columns = colmns
|
|
612
613
|
table_html = _metrics_table_html(rpt_df)
|
|
613
614
|
|
|
614
|
-
dirpath = _get_output_directory(directory)
|
|
615
|
+
dirpath = _get_output_directory(directory=directory)
|
|
615
616
|
|
|
616
617
|
if not filename:
|
|
617
618
|
filename = "".join(choice(ascii_letters) for _ in range(6)) + ".html"
|
|
@@ -625,16 +626,16 @@ def report_html(
|
|
|
625
626
|
)
|
|
626
627
|
|
|
627
628
|
line_layout, bar_layout = _get_plotly_layouts(
|
|
628
|
-
layout_theme,
|
|
629
|
-
colorway,
|
|
630
|
-
copied.item_count,
|
|
629
|
+
layout_theme=layout_theme,
|
|
630
|
+
colorway=colorway,
|
|
631
|
+
item_count=copied.item_count,
|
|
631
632
|
)
|
|
632
633
|
|
|
633
634
|
config = cast("dict[str, Any]", fig_theme.get("config", {})) or {}
|
|
634
635
|
config = {**config, "responsive": True, "displayModeBar": False}
|
|
635
636
|
|
|
636
|
-
plotly_script = _get_plotly_script(include_plotlyjs)
|
|
637
|
-
logo_html = _get_logo_html(logo, add_logo=add_logo)
|
|
637
|
+
plotly_script = _get_plotly_script(include_plotlyjs=include_plotlyjs)
|
|
638
|
+
logo_html = _get_logo_html(logo=logo, add_logo=add_logo)
|
|
638
639
|
css = _get_css()
|
|
639
640
|
|
|
640
641
|
line_payload = {
|
|
@@ -648,21 +649,21 @@ def report_html(
|
|
|
648
649
|
"config": config,
|
|
649
650
|
}
|
|
650
651
|
|
|
651
|
-
legend_html = _get_legend_html(line_traces, colorway)
|
|
652
|
+
legend_html = _get_legend_html(line_traces=line_traces, colorway=colorway)
|
|
652
653
|
|
|
653
654
|
html = _generate_html(
|
|
654
|
-
title,
|
|
655
|
-
css,
|
|
656
|
-
plotly_script,
|
|
657
|
-
logo_html,
|
|
658
|
-
table_html,
|
|
659
|
-
line_payload,
|
|
660
|
-
bar_payload,
|
|
661
|
-
legend_html,
|
|
655
|
+
title=title,
|
|
656
|
+
css=css,
|
|
657
|
+
plotly_script=plotly_script,
|
|
658
|
+
logo_html=logo_html,
|
|
659
|
+
table_html=table_html,
|
|
660
|
+
line_payload=line_payload,
|
|
661
|
+
bar_payload=bar_payload,
|
|
662
|
+
legend_html=legend_html,
|
|
662
663
|
)
|
|
663
664
|
|
|
664
665
|
if output_type == "file":
|
|
665
|
-
output = _write_html_file(plotfile, html, auto_open=auto_open)
|
|
666
|
+
output = _write_html_file(plotfile=plotfile, html=html, auto_open=auto_open)
|
|
666
667
|
else:
|
|
667
668
|
output = html
|
|
668
669
|
|
|
@@ -32,6 +32,7 @@ from pandas import (
|
|
|
32
32
|
date_range,
|
|
33
33
|
)
|
|
34
34
|
from pydantic import field_validator, model_validator
|
|
35
|
+
from scipy.stats import norm
|
|
35
36
|
|
|
36
37
|
from ._common_model import _calculate_time_factor, _CommonModel
|
|
37
38
|
from .datefixer import _do_resample_to_business_period_ends, date_fix
|
|
@@ -625,7 +626,7 @@ class OpenTimeSeries(_CommonModel[float]):
|
|
|
625
626
|
cast("Timestamp", earlier) : cast("Timestamp", later)
|
|
626
627
|
].copy()
|
|
627
628
|
|
|
628
|
-
data.loc[:, (self.label, ValueType.RTRN)] = log(
|
|
629
|
+
data.loc[:, (self.label, ValueType.RTRN)] = log(
|
|
629
630
|
data.loc[:, self.tsdf.columns.to_numpy()[0]],
|
|
630
631
|
).diff()
|
|
631
632
|
|
|
@@ -647,7 +648,83 @@ class OpenTimeSeries(_CommonModel[float]):
|
|
|
647
648
|
return Series(
|
|
648
649
|
data=rawdata,
|
|
649
650
|
index=data.index,
|
|
650
|
-
name=(self.label, ValueType.
|
|
651
|
+
name=(self.label, ValueType.EWMA_VOL),
|
|
652
|
+
dtype="float64",
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
def ewma_var_func(
|
|
656
|
+
self: Self,
|
|
657
|
+
lmbda: float = 0.94,
|
|
658
|
+
day_chunk: int = 11,
|
|
659
|
+
level: float = 0.95,
|
|
660
|
+
dlta_degr_freedms: int = 0,
|
|
661
|
+
months_from_last: int | None = None,
|
|
662
|
+
from_date: dt.date | None = None,
|
|
663
|
+
to_date: dt.date | None = None,
|
|
664
|
+
periods_in_a_year_fixed: DaysInYearType | None = None,
|
|
665
|
+
) -> Series[float]:
|
|
666
|
+
"""Exponentially Weighted Moving Average Model for Value At Risk (VaR).
|
|
667
|
+
|
|
668
|
+
Reference: https://www.investopedia.com/articles/07/ewma.asp.
|
|
669
|
+
|
|
670
|
+
Args:
|
|
671
|
+
lmbda: Scaling factor to determine weighting. Defaults to 0.94.
|
|
672
|
+
day_chunk: Sampling the data which is assumed to be daily.
|
|
673
|
+
Defaults to 11.
|
|
674
|
+
level: The sought VaR level. Defaults to 0.95.
|
|
675
|
+
dlta_degr_freedms: Variance bias factor taking the value 0 or 1.
|
|
676
|
+
Defaults to 0.
|
|
677
|
+
months_from_last: Number of months offset as positive integer.
|
|
678
|
+
Overrides use of from_date and to_date. Optional.
|
|
679
|
+
from_date: Specific from date. Optional.
|
|
680
|
+
to_date: Specific to date. Optional.
|
|
681
|
+
periods_in_a_year_fixed: Allows locking the periods-in-a-year to simplify
|
|
682
|
+
test cases and comparisons. Optional.
|
|
683
|
+
|
|
684
|
+
Returns:
|
|
685
|
+
Series EWMA VaR.
|
|
686
|
+
"""
|
|
687
|
+
earlier, later = self.calc_range(
|
|
688
|
+
months_offset=months_from_last,
|
|
689
|
+
from_dt=from_date,
|
|
690
|
+
to_dt=to_date,
|
|
691
|
+
)
|
|
692
|
+
time_factor = _calculate_time_factor(
|
|
693
|
+
data=self.tsdf.loc[
|
|
694
|
+
cast("Timestamp", earlier) : cast("Timestamp", later)
|
|
695
|
+
].iloc[:, 0],
|
|
696
|
+
earlier=earlier,
|
|
697
|
+
later=later,
|
|
698
|
+
periods_in_a_year_fixed=periods_in_a_year_fixed,
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
data = self.tsdf.loc[
|
|
702
|
+
cast("Timestamp", earlier) : cast("Timestamp", later)
|
|
703
|
+
].copy()
|
|
704
|
+
|
|
705
|
+
data.loc[:, (self.label, ValueType.RTRN)] = log(
|
|
706
|
+
data.loc[:, self.tsdf.columns.to_numpy()[0]],
|
|
707
|
+
).diff()
|
|
708
|
+
|
|
709
|
+
rawdata = [
|
|
710
|
+
data[(self.label, ValueType.RTRN)]
|
|
711
|
+
.iloc[1:day_chunk]
|
|
712
|
+
.std(ddof=dlta_degr_freedms)
|
|
713
|
+
* sqrt(time_factor),
|
|
714
|
+
]
|
|
715
|
+
|
|
716
|
+
for item in data[(self.label, ValueType.RTRN)].iloc[1:]:
|
|
717
|
+
prev = rawdata[-1]
|
|
718
|
+
rawdata.append(
|
|
719
|
+
sqrt(
|
|
720
|
+
square(item) * time_factor * (1 - lmbda) + square(prev) * lmbda,
|
|
721
|
+
),
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
return Series(
|
|
725
|
+
data=array(rawdata) * norm.ppf(1 - level),
|
|
726
|
+
index=data.index,
|
|
727
|
+
name=(self.label, ValueType.EWMA_VAR),
|
|
651
728
|
dtype="float64",
|
|
652
729
|
)
|
|
653
730
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "openseries"
|
|
3
|
-
version = "2.1.
|
|
3
|
+
version = "2.1.6"
|
|
4
4
|
description = "Tools for analyzing financial timeseries."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Martin Karrin", email = "martin.karrin@captor.se" },
|
|
@@ -67,7 +67,7 @@ pre-commit = ">=4.5.1"
|
|
|
67
67
|
pytest = ">=9.0.2"
|
|
68
68
|
pytest-cov = ">=7.0.0"
|
|
69
69
|
pytest-xdist = ">=3.8.0"
|
|
70
|
-
ruff = "0.
|
|
70
|
+
ruff = "0.15.0"
|
|
71
71
|
types-openpyxl = ">=3.1.2"
|
|
72
72
|
scipy-stubs = ">=1.14.1.0"
|
|
73
73
|
types-python-dateutil = ">=2.8.2"
|
|
@@ -80,7 +80,7 @@ sphinx-autodoc-typehints = ">=3.6.0"
|
|
|
80
80
|
sphinx-rtd-theme = ">=3.1.0rc1"
|
|
81
81
|
|
|
82
82
|
[build-system]
|
|
83
|
-
requires = ["poetry-core>=2.
|
|
83
|
+
requires = ["poetry-core>=2.3.1"]
|
|
84
84
|
build-backend = "poetry.core.masonry.api"
|
|
85
85
|
|
|
86
86
|
[tool.setuptools.package-data]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|