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/__init__.py +43 -0
- openseries/_common_model.py +6 -5
- openseries/datefixer.py +10 -0
- openseries/frame.py +8 -573
- openseries/load_plotly.py +2 -0
- openseries/portfoliotools.py +613 -0
- openseries/series.py +2 -0
- openseries/simulation.py +7 -5
- openseries/types.py +20 -0
- {openseries-1.5.7.dist-info → openseries-1.6.0.dist-info}/METADATA +1 -1
- openseries-1.6.0.dist-info/RECORD +16 -0
- openseries-1.5.7.dist-info/RECORD +0 -15
- {openseries-1.5.7.dist-info → openseries-1.6.0.dist-info}/LICENSE.md +0 -0
- {openseries-1.5.7.dist-info → openseries-1.6.0.dist-info}/WHEEL +0 -0
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
|
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
|
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,
|
486
|
-
tail=tail,
|
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
|