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