openseries 1.5.7__py3-none-any.whl → 1.6.0__py3-none-any.whl

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/frame.py CHANGED
@@ -6,34 +6,22 @@ from __future__ import annotations
6
6
  import datetime as dt
7
7
  from copy import deepcopy
8
8
  from functools import reduce
9
- from inspect import stack
10
9
  from logging import warning
11
- from pathlib import Path
12
- from typing import Callable, Optional, Union, cast
10
+ from typing import Optional, Union, cast
13
11
 
14
12
  import statsmodels.api as sm # type: ignore[import-untyped,unused-ignore]
15
13
  from numpy import (
16
- append,
17
14
  array,
18
15
  cov,
19
16
  cumprod,
20
17
  divide,
21
- dot,
22
- float64,
23
- inf,
24
18
  isinf,
25
- linspace,
26
19
  log,
27
20
  nan,
28
21
  sqrt,
29
22
  square,
30
23
  std,
31
- zeros,
32
24
  )
33
- from numpy import (
34
- sum as npsum,
35
- )
36
- from numpy.typing import NDArray
37
25
  from pandas import (
38
26
  DataFrame,
39
27
  DatetimeIndex,
@@ -44,11 +32,7 @@ from pandas import (
44
32
  concat,
45
33
  merge,
46
34
  )
47
- from plotly.graph_objs import Figure # type: ignore[import-untyped,unused-ignore]
48
- from plotly.io import to_html # type: ignore[import-untyped,unused-ignore]
49
- from plotly.offline import plot # type: ignore[import-untyped,unused-ignore]
50
- from pydantic import DirectoryPath, field_validator
51
- from scipy.optimize import minimize # type: ignore[import-untyped,unused-ignore]
35
+ from pydantic import field_validator
52
36
 
53
37
  # noinspection PyProtectedMember
54
38
  from statsmodels.regression.linear_model import ( # type: ignore[import-untyped,unused-ignore]
@@ -58,9 +42,7 @@ from typing_extensions import Self
58
42
 
59
43
  from openseries._common_model import _CommonModel
60
44
  from openseries.datefixer import do_resample_to_business_period_ends
61
- from openseries.load_plotly import load_plotly_dict
62
45
  from openseries.series import OpenTimeSeries
63
- from openseries.simulation import random_generator
64
46
  from openseries.types import (
65
47
  CountriesType,
66
48
  DaysInYearType,
@@ -68,18 +50,17 @@ from openseries.types import (
68
50
  LiteralCaptureRatio,
69
51
  LiteralFrameProps,
70
52
  LiteralHowMerge,
71
- LiteralLinePlotMode,
72
53
  LiteralOlsFitCovType,
73
54
  LiteralOlsFitMethod,
74
55
  LiteralPandasReindexMethod,
75
- LiteralPlotlyJSlib,
76
- LiteralPlotlyOutput,
77
56
  LiteralPortfolioWeightings,
78
57
  LiteralTrunc,
79
58
  OpenFramePropertiesList,
80
59
  ValueType,
81
60
  )
82
61
 
62
+ __all__ = ["OpenFrame"]
63
+
83
64
 
84
65
  # noinspection PyUnresolvedReferences
85
66
  class OpenFrame(_CommonModel):
@@ -478,12 +459,12 @@ class OpenFrame(_CommonModel):
478
459
  An OpenFrame object
479
460
 
480
461
  """
481
- head = self.tsdf.loc[self.first_indices.max()].copy()
482
- tail = self.tsdf.loc[self.last_indices.min()].copy()
462
+ head: Series[float] = self.tsdf.loc[self.first_indices.max()].copy()
463
+ tail: Series[float] = self.tsdf.loc[self.last_indices.min()].copy()
483
464
  dates = do_resample_to_business_period_ends(
484
465
  data=self.tsdf,
485
- head=head, # type: ignore[arg-type,unused-ignore]
486
- tail=tail, # type: ignore[arg-type,unused-ignore]
466
+ head=head,
467
+ tail=tail,
487
468
  freq=freq,
488
469
  countries=countries,
489
470
  )
@@ -1692,549 +1673,3 @@ class OpenFrame(_CommonModel):
1692
1673
  )
1693
1674
 
1694
1675
  return DataFrame(corrdf)
1695
-
1696
-
1697
- def simulate_portfolios(
1698
- simframe: OpenFrame,
1699
- num_ports: int,
1700
- seed: int,
1701
- ) -> DataFrame:
1702
- """
1703
- Generate random weights for simulated portfolios.
1704
-
1705
- Parameters
1706
- ----------
1707
- simframe: OpenFrame
1708
- Return data for portfolio constituents
1709
- num_ports: int
1710
- Number of possible portfolios to simulate
1711
- seed: int
1712
- The seed for the random process
1713
-
1714
- Returns
1715
- -------
1716
- pandas.DataFrame
1717
- The resulting data
1718
-
1719
- """
1720
- copi = simframe.from_deepcopy()
1721
-
1722
- if any(
1723
- x == ValueType.PRICE for x in copi.tsdf.columns.get_level_values(1).to_numpy()
1724
- ):
1725
- copi.value_to_ret()
1726
- log_ret = copi.tsdf.copy()[1:]
1727
- else:
1728
- log_ret = copi.tsdf.copy()
1729
-
1730
- log_ret.columns = log_ret.columns.droplevel(level=1)
1731
-
1732
- randomizer = random_generator(seed=seed)
1733
-
1734
- all_weights = zeros((num_ports, simframe.item_count))
1735
- ret_arr = zeros(num_ports)
1736
- vol_arr = zeros(num_ports)
1737
- sharpe_arr = zeros(num_ports)
1738
-
1739
- for x in range(num_ports):
1740
- weights = array(randomizer.random(simframe.item_count))
1741
- weights = weights / npsum(weights)
1742
- all_weights[x, :] = weights
1743
-
1744
- vol_arr[x] = sqrt(
1745
- dot(
1746
- weights.T,
1747
- dot(log_ret.cov() * simframe.periods_in_a_year, weights),
1748
- ),
1749
- )
1750
-
1751
- ret_arr[x] = npsum(log_ret.mean() * weights * simframe.periods_in_a_year)
1752
-
1753
- sharpe_arr[x] = ret_arr[x] / vol_arr[x]
1754
-
1755
- # noinspection PyUnreachableCode
1756
- simdf = concat(
1757
- [
1758
- DataFrame({"stdev": vol_arr, "ret": ret_arr, "sharpe": sharpe_arr}),
1759
- DataFrame(all_weights, columns=simframe.columns_lvl_zero),
1760
- ],
1761
- axis="columns",
1762
- )
1763
- simdf = simdf.replace([inf, -inf], nan)
1764
- return simdf.dropna()
1765
-
1766
-
1767
- def efficient_frontier( # noqa: C901
1768
- eframe: OpenFrame,
1769
- num_ports: int = 5000,
1770
- seed: int = 71,
1771
- upperbounds: float = 1.0,
1772
- frontier_points: int = 200,
1773
- *,
1774
- tweak: bool = True,
1775
- ) -> tuple[DataFrame, DataFrame, NDArray[float64]]:
1776
- """
1777
- Identify an efficient frontier.
1778
-
1779
- Parameters
1780
- ----------
1781
- eframe: OpenFrame
1782
- Portfolio data
1783
- num_ports: int, default: 5000
1784
- Number of possible portfolios to simulate
1785
- seed: int, default: 71
1786
- The seed for the random process
1787
- upperbounds: float, default: 1.0
1788
- The largest allowed allocation to a single asset
1789
- frontier_points: int, default: 200
1790
- number of points along frontier to optimize
1791
- tweak: bool, default: True
1792
- cutting the frontier to exclude multiple points with almost the same risk
1793
-
1794
- Returns
1795
- -------
1796
- tuple[DataFrame, DataFrame, NDArray[float]]
1797
- The efficient frontier data, simulation data and optimal portfolio
1798
-
1799
- """
1800
- if eframe.weights is None:
1801
- eframe.weights = [1.0 / eframe.item_count] * eframe.item_count
1802
-
1803
- copi = eframe.from_deepcopy()
1804
-
1805
- if any(
1806
- x == ValueType.PRICE for x in copi.tsdf.columns.get_level_values(1).to_numpy()
1807
- ):
1808
- copi.value_to_ret()
1809
- log_ret = copi.tsdf.copy()[1:]
1810
- else:
1811
- log_ret = copi.tsdf.copy()
1812
-
1813
- log_ret.columns = log_ret.columns.droplevel(level=1)
1814
-
1815
- simulated = simulate_portfolios(simframe=copi, num_ports=num_ports, seed=seed)
1816
-
1817
- frontier_min = simulated.loc[simulated["stdev"].idxmin()]["ret"]
1818
- arithmetic_mean = log_ret.mean() * copi.periods_in_a_year
1819
- frontier_max = 0.0
1820
- if isinstance(arithmetic_mean, Series):
1821
- frontier_max = arithmetic_mean.max()
1822
-
1823
- def _check_sum(weights: NDArray[float64]) -> float64:
1824
- return cast(float64, npsum(weights) - 1)
1825
-
1826
- def _get_ret_vol_sr(
1827
- lg_ret: DataFrame,
1828
- weights: NDArray[float64],
1829
- per_in_yr: float,
1830
- ) -> NDArray[float64]:
1831
- ret = npsum(lg_ret.mean() * weights) * per_in_yr
1832
- volatility = sqrt(dot(weights.T, dot(lg_ret.cov() * per_in_yr, weights)))
1833
- sr = ret / volatility
1834
- return cast(NDArray[float64], array([ret, volatility, sr]))
1835
-
1836
- def _diff_return(
1837
- lg_ret: DataFrame,
1838
- weights: NDArray[float64],
1839
- per_in_yr: float,
1840
- poss_return: float,
1841
- ) -> float64:
1842
- return cast(
1843
- float64,
1844
- _get_ret_vol_sr(lg_ret=lg_ret, weights=weights, per_in_yr=per_in_yr)[0]
1845
- - poss_return,
1846
- )
1847
-
1848
- def _neg_sharpe(weights: NDArray[float64]) -> float64:
1849
- return cast(
1850
- float64,
1851
- _get_ret_vol_sr(
1852
- lg_ret=log_ret,
1853
- weights=weights,
1854
- per_in_yr=eframe.periods_in_a_year,
1855
- )[2]
1856
- * -1,
1857
- )
1858
-
1859
- def _minimize_volatility(
1860
- weights: NDArray[float64],
1861
- ) -> float64:
1862
- return cast(
1863
- float64,
1864
- _get_ret_vol_sr(
1865
- lg_ret=log_ret,
1866
- weights=weights,
1867
- per_in_yr=eframe.periods_in_a_year,
1868
- )[1],
1869
- )
1870
-
1871
- constraints = {"type": "eq", "fun": _check_sum}
1872
- bounds = tuple((0, upperbounds) for _ in range(eframe.item_count))
1873
- init_guess = array(eframe.weights)
1874
-
1875
- opt_results = minimize(
1876
- fun=_neg_sharpe,
1877
- x0=init_guess,
1878
- method="SLSQP",
1879
- bounds=bounds,
1880
- constraints=constraints,
1881
- )
1882
-
1883
- optimal = _get_ret_vol_sr(
1884
- lg_ret=log_ret,
1885
- weights=opt_results.x,
1886
- per_in_yr=eframe.periods_in_a_year,
1887
- )
1888
-
1889
- frontier_y = linspace(start=frontier_min, stop=frontier_max, num=frontier_points)
1890
- frontier_x = []
1891
- frontier_weights = []
1892
-
1893
- for possible_return in frontier_y:
1894
- cons = cast(
1895
- dict[str, Union[str, Callable[[float, NDArray[float64]], float64]]],
1896
- (
1897
- {"type": "eq", "fun": _check_sum},
1898
- {
1899
- "type": "eq",
1900
- "fun": lambda w, poss_return=possible_return: _diff_return(
1901
- lg_ret=log_ret,
1902
- weights=w,
1903
- per_in_yr=eframe.periods_in_a_year,
1904
- poss_return=poss_return,
1905
- ),
1906
- },
1907
- ),
1908
- )
1909
-
1910
- result = minimize(
1911
- fun=_minimize_volatility,
1912
- x0=init_guess,
1913
- method="SLSQP",
1914
- bounds=bounds,
1915
- constraints=cons,
1916
- )
1917
-
1918
- frontier_x.append(result["fun"])
1919
- frontier_weights.append(result["x"])
1920
-
1921
- # noinspection PyUnreachableCode
1922
- line_df = concat(
1923
- [
1924
- DataFrame(data=frontier_weights, columns=eframe.columns_lvl_zero),
1925
- DataFrame({"stdev": frontier_x, "ret": frontier_y}),
1926
- ],
1927
- axis="columns",
1928
- )
1929
- line_df["sharpe"] = line_df.ret / line_df.stdev
1930
-
1931
- limit_small = 0.0001
1932
- line_df = line_df.mask(line_df.abs() < limit_small, 0.0)
1933
- line_df["text"] = line_df.apply(
1934
- lambda c: "<br><br>Weights:<br>"
1935
- + "<br>".join(
1936
- [f"{c[nm]:.1%} {nm}" for nm in eframe.columns_lvl_zero],
1937
- ),
1938
- axis="columns",
1939
- )
1940
-
1941
- if tweak:
1942
- limit_tweak = 0.001
1943
- line_df["stdev_diff"] = line_df.stdev.pct_change()
1944
- line_df = line_df.loc[line_df.stdev_diff.abs() > limit_tweak]
1945
- line_df = line_df.drop(columns="stdev_diff")
1946
-
1947
- return line_df, simulated, append(optimal, opt_results.x)
1948
-
1949
-
1950
- def constrain_optimized_portfolios(
1951
- data: OpenFrame,
1952
- serie: OpenTimeSeries,
1953
- portfolioname: str = "Current Portfolio",
1954
- simulations: int = 10000,
1955
- curve_points: int = 200,
1956
- upper_bound: float = 0.25,
1957
- ) -> tuple[OpenFrame, OpenTimeSeries, OpenFrame, OpenTimeSeries]:
1958
- """
1959
- Constrain optimized portfolios to those that improve on the current one.
1960
-
1961
- Parameters
1962
- ----------
1963
- data: OpenFrame
1964
- Portfolio data
1965
- serie: OpenTimeSeries
1966
- A
1967
- portfolioname: str, default: "Current Portfolio"
1968
- Name of the portfolio
1969
- simulations: int, default: 10000
1970
- Number of possible portfolios to simulate
1971
- curve_points: int, default: 200
1972
- Number of optimal portfolios on the efficient frontier
1973
- upper_bound: float, default: 0.25
1974
- The largest allowed allocation to a single asset
1975
-
1976
- Returns
1977
- -------
1978
- tuple[OpenFrame, OpenTimeSeries, OpenFrame, OpenTimeSeries]
1979
- The constrained optimal portfolio data
1980
-
1981
- """
1982
- lr_frame = data.from_deepcopy()
1983
- mv_frame = data.from_deepcopy()
1984
-
1985
- front_frame, sim_frame, optimal = efficient_frontier(
1986
- eframe=data,
1987
- num_ports=simulations,
1988
- frontier_points=curve_points,
1989
- upperbounds=upper_bound,
1990
- )
1991
-
1992
- condition_least_ret = front_frame.ret > serie.arithmetic_ret
1993
- # noinspection PyArgumentList
1994
- least_ret_frame = front_frame[condition_least_ret].sort_values(by="stdev")
1995
- least_ret_port = least_ret_frame.iloc[0]
1996
- least_ret_port_name = f"Minimize vol & target return of {portfolioname}"
1997
- least_ret_weights = [least_ret_port[c] for c in lr_frame.columns_lvl_zero]
1998
- lr_frame.weights = least_ret_weights
1999
- resleast = OpenTimeSeries.from_df(lr_frame.make_portfolio(least_ret_port_name))
2000
-
2001
- condition_most_vol = front_frame.stdev < serie.vol
2002
- # noinspection PyArgumentList
2003
- most_vol_frame = front_frame[condition_most_vol].sort_values(
2004
- by="ret",
2005
- ascending=False,
2006
- )
2007
- most_vol_port = most_vol_frame.iloc[0]
2008
- most_vol_port_name = f"Maximize return & target risk of {portfolioname}"
2009
- most_vol_weights = [most_vol_port[c] for c in mv_frame.columns_lvl_zero]
2010
- mv_frame.weights = most_vol_weights
2011
- resmost = OpenTimeSeries.from_df(mv_frame.make_portfolio(most_vol_port_name))
2012
-
2013
- return lr_frame, resleast, mv_frame, resmost
2014
-
2015
-
2016
- def prepare_plot_data(
2017
- assets: OpenFrame,
2018
- current: OpenTimeSeries,
2019
- optimized: NDArray[float64],
2020
- ) -> DataFrame:
2021
- """
2022
- Prepare date to be used as point_frame in the sharpeplot function.
2023
-
2024
- Parameters
2025
- ----------
2026
- assets: OpenFrame
2027
- Portfolio data with individual assets and a weighted portfolio
2028
- current: OpenTimeSeries
2029
- The current or initial portfolio based on given weights
2030
- optimized: DataFrame
2031
- Data optimized with the efficient_frontier method
2032
-
2033
- Returns
2034
- -------
2035
- DataFrame
2036
- The data prepared with mean returns, volatility and weights
2037
-
2038
- """
2039
- txt = "<br><br>Weights:<br>" + "<br>".join(
2040
- [
2041
- f"{wgt:.1%} {nm}"
2042
- for wgt, nm in zip(
2043
- cast(list[float], assets.weights),
2044
- assets.columns_lvl_zero,
2045
- )
2046
- ],
2047
- )
2048
-
2049
- opt_text_list = [
2050
- f"{wgt:.1%} {nm}" for wgt, nm in zip(optimized[3:], assets.columns_lvl_zero)
2051
- ]
2052
- opt_text = "<br><br>Weights:<br>" + "<br>".join(opt_text_list)
2053
- vol: Series[float] = assets.vol
2054
- plotframe = DataFrame(
2055
- data=[
2056
- assets.arithmetic_ret,
2057
- vol,
2058
- Series(
2059
- data=[""] * assets.item_count,
2060
- index=vol.index,
2061
- ),
2062
- ],
2063
- index=["ret", "stdev", "text"],
2064
- )
2065
- plotframe.columns = plotframe.columns.droplevel(level=1)
2066
- plotframe["Max Sharpe Portfolio"] = [optimized[0], optimized[1], opt_text]
2067
- plotframe[current.label] = [current.arithmetic_ret, current.vol, txt]
2068
-
2069
- return plotframe
2070
-
2071
-
2072
- def sharpeplot( # noqa: C901
2073
- sim_frame: DataFrame = None,
2074
- line_frame: DataFrame = None,
2075
- point_frame: DataFrame = None,
2076
- point_frame_mode: LiteralLinePlotMode = "markers",
2077
- filename: Optional[str] = None,
2078
- directory: Optional[DirectoryPath] = None,
2079
- titletext: Optional[str] = None,
2080
- output_type: LiteralPlotlyOutput = "file",
2081
- include_plotlyjs: LiteralPlotlyJSlib = "cdn",
2082
- *,
2083
- title: bool = True,
2084
- add_logo: bool = True,
2085
- auto_open: bool = True,
2086
- ) -> tuple[Figure, str]:
2087
- """
2088
- Create scatter plot coloured by Sharpe Ratio.
2089
-
2090
- Parameters
2091
- ----------
2092
- sim_frame: DataFrame, optional
2093
- Data from the simulate_portfolios method.
2094
- line_frame: DataFrame, optional
2095
- Data from the efficient_frontier method.
2096
- point_frame: DataFrame, optional
2097
- Data to highlight current and efficient portfolios.
2098
- point_frame_mode: LiteralLinePlotMode, default: markers
2099
- Which type of scatter to use.
2100
- filename: str, optional
2101
- Name of the Plotly html file
2102
- directory: DirectoryPath, optional
2103
- Directory where Plotly html file is saved
2104
- titletext: str, optional
2105
- Text for the plot title
2106
- output_type: LiteralPlotlyOutput, default: "file"
2107
- Determines output type
2108
- include_plotlyjs: LiteralPlotlyJSlib, default: "cdn"
2109
- Determines how the plotly.js library is included in the output
2110
- title: bool, default: True
2111
- Whether to add standard plot title
2112
- add_logo: bool, default: True
2113
- Whether to add Captor logo
2114
- auto_open: bool, default: True
2115
- Determines whether to open a browser window with the plot
2116
-
2117
- Returns
2118
- -------
2119
- Figure
2120
- The scatter plot with simulated and optimized results
2121
-
2122
- """
2123
- returns = []
2124
- risk = []
2125
-
2126
- if directory:
2127
- dirpath = Path(directory).resolve()
2128
- elif Path.home().joinpath("Documents").exists():
2129
- dirpath = Path.home().joinpath("Documents")
2130
- else:
2131
- dirpath = Path(stack()[1].filename).parent
2132
-
2133
- if not filename:
2134
- filename = "sharpeplot.html"
2135
- plotfile = dirpath.joinpath(filename)
2136
-
2137
- fig, logo = load_plotly_dict()
2138
- figure = Figure(fig)
2139
-
2140
- if sim_frame is not None:
2141
- returns.extend(list(sim_frame.loc[:, "ret"]))
2142
- risk.extend(list(sim_frame.loc[:, "stdev"]))
2143
- figure.add_scatter(
2144
- x=sim_frame.loc[:, "stdev"],
2145
- y=sim_frame.loc[:, "ret"],
2146
- hoverinfo="skip",
2147
- marker={
2148
- "size": 10,
2149
- "opacity": 0.5,
2150
- "color": sim_frame.loc[:, "sharpe"],
2151
- "colorscale": "Jet",
2152
- "reversescale": True,
2153
- "colorbar": {"thickness": 20, "title": "Ratio<br>ret / vol"},
2154
- },
2155
- mode="markers",
2156
- name="simulated portfolios",
2157
- )
2158
- if line_frame is not None:
2159
- returns.extend(list(line_frame.loc[:, "ret"]))
2160
- risk.extend(list(line_frame.loc[:, "stdev"]))
2161
- figure.add_scatter(
2162
- x=line_frame.loc[:, "stdev"],
2163
- y=line_frame.loc[:, "ret"],
2164
- text=line_frame.loc[:, "text"],
2165
- xhoverformat=".2%",
2166
- yhoverformat=".2%",
2167
- hovertemplate="Return %{y}<br>Vol %{x}%{text}",
2168
- hoverlabel_align="right",
2169
- line={"width": 2.5, "dash": "solid"},
2170
- mode="lines",
2171
- name="Efficient frontier",
2172
- )
2173
-
2174
- colorway = cast(dict[str, list[str]], fig["layout"]).get("colorway")[
2175
- : len(point_frame.columns)
2176
- ]
2177
-
2178
- if point_frame is not None:
2179
- for col, clr in zip(point_frame.columns, colorway):
2180
- returns.extend([point_frame.loc["ret", col]])
2181
- risk.extend([point_frame.loc["stdev", col]])
2182
- figure.add_scatter(
2183
- x=[point_frame.loc["stdev", col]],
2184
- y=[point_frame.loc["ret", col]],
2185
- xhoverformat=".2%",
2186
- yhoverformat=".2%",
2187
- hovertext=[point_frame.loc["text", col]],
2188
- hovertemplate="Return %{y}<br>Vol %{x}%{hovertext}",
2189
- hoverlabel_align="right",
2190
- marker={"size": 20, "color": clr},
2191
- mode=point_frame_mode,
2192
- name=col,
2193
- text=col,
2194
- textfont={"size": 14},
2195
- textposition="bottom center",
2196
- )
2197
-
2198
- figure.update_layout(
2199
- xaxis={"tickformat": ".1%"},
2200
- xaxis_title="volatility",
2201
- yaxis={
2202
- "tickformat": ".1%",
2203
- "scaleanchor": "x",
2204
- "scaleratio": 1,
2205
- },
2206
- yaxis_title="annual return",
2207
- showlegend=False,
2208
- )
2209
- if title:
2210
- if titletext is None:
2211
- titletext = "<b>Risk and Return</b><br>"
2212
- figure.update_layout(title={"text": titletext, "font": {"size": 32}})
2213
-
2214
- if add_logo:
2215
- figure.add_layout_image(logo)
2216
-
2217
- if output_type == "file":
2218
- plot(
2219
- figure_or_data=figure,
2220
- filename=str(plotfile),
2221
- auto_open=auto_open,
2222
- auto_play=False,
2223
- link_text="",
2224
- include_plotlyjs=cast(bool, include_plotlyjs),
2225
- config=fig["config"],
2226
- output_type=output_type,
2227
- )
2228
- string_output = str(plotfile)
2229
- else:
2230
- div_id = filename.split(sep=".")[0]
2231
- string_output = to_html(
2232
- fig=figure,
2233
- config=fig["config"],
2234
- auto_play=False,
2235
- include_plotlyjs=cast(bool, include_plotlyjs),
2236
- full_html=False,
2237
- div_id=div_id,
2238
- )
2239
-
2240
- return figure, string_output
openseries/load_plotly.py CHANGED
@@ -11,6 +11,8 @@ from requests.exceptions import ConnectionError
11
11
 
12
12
  from openseries.types import CaptorLogoType, PlotlyLayoutType
13
13
 
14
+ __all__ = ["load_plotly_dict"]
15
+
14
16
 
15
17
  def _check_remote_file_existence(url: str) -> bool:
16
18
  """