mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-11-02 07:23:25 +00:00
* Began the fax * Bumped ruff * Fixed a BUNCH of ruff errors * Fixed a lot of ruff errors * Simplify if statements * Fixed black * Updated some stuff * Updated some stuff * A lot more ruff fixes * Added a fix * Added a fix * Fixed mypy * Fixed some stuff * Reverted one thing * Small fix * Small fix * Small fix * Added fix * Added fix * Update requirements
720 lines
24 KiB
Python
720 lines
24 KiB
Python
"""Optimization View"""
|
|
__docformat__ = "numpy"
|
|
|
|
# pylint: disable=R0913, R0914, C0302, too-many-branches, too-many-statements, line-too-long
|
|
# flake8: noqa: E501
|
|
|
|
# IMPORTS STANDARD
|
|
import logging
|
|
import math
|
|
import warnings
|
|
from typing import Optional
|
|
|
|
# IMPORTATION THIRDPARTY
|
|
import matplotlib
|
|
import matplotlib.pyplot as plt
|
|
import numpy as np
|
|
import pandas as pd
|
|
import riskfolio as rp
|
|
from matplotlib.gridspec import GridSpec
|
|
from matplotlib.lines import Line2D
|
|
|
|
from openbb_terminal.config_terminal import theme
|
|
|
|
# IMPORTATION INTERNAL
|
|
from openbb_terminal.core.session.current_user import get_current_user
|
|
from openbb_terminal.decorators import log_start_end
|
|
from openbb_terminal.helper_funcs import plot_autoscale
|
|
from openbb_terminal.portfolio.portfolio_optimization.optimizer_helper import get_kwarg
|
|
from openbb_terminal.portfolio.portfolio_optimization.po_engine import PoEngine
|
|
from openbb_terminal.portfolio.portfolio_optimization.po_model import (
|
|
get_ef,
|
|
validate_inputs,
|
|
)
|
|
from openbb_terminal.portfolio.portfolio_optimization.statics import (
|
|
RISK_CHOICES,
|
|
TIME_FACTOR,
|
|
)
|
|
from openbb_terminal.rich_config import console
|
|
|
|
warnings.filterwarnings("ignore")
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def display_ef(portfolio_engine: Optional[PoEngine] = None, **kwargs):
|
|
"""Display efficient frontier
|
|
|
|
Parameters
|
|
----------
|
|
portfolio_engine : PoEngine, optional
|
|
Portfolio optimization engine, by default None
|
|
Use `portfolio.po.load` to load a portfolio engine
|
|
interval : str, optional
|
|
Interval to get data, by default '3y'
|
|
start_date : str, optional
|
|
If not using interval, start date string (YYYY-MM-DD), by default ""
|
|
end_date : str, optional
|
|
If not using interval, end date string (YYYY-MM-DD). If empty use last weekday, by default ""
|
|
log_returns : bool, optional
|
|
If True use log returns, else arithmetic returns, by default False
|
|
freq : str, optional
|
|
Frequency of returns, by default 'D'. Options: 'D' for daily, 'W' for weekly, 'M' for monthly
|
|
maxnan: float, optional
|
|
Maximum percentage of NaNs allowed in the data, by default 0.05
|
|
threshold: float, optional
|
|
Value used to replace outliers that are higher than threshold, by default 0.0
|
|
method: str, optional
|
|
Method used to fill nan values, by default 'time'
|
|
For more information see `interpolate <https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.interpolate.html>`__.
|
|
value : float, optional
|
|
Amount to allocate to portfolio in long positions, by default 1.0
|
|
value_short : float, optional
|
|
Amount to allocate to portfolio in short positions, by default 0.0
|
|
risk_measure: str, optional
|
|
The risk measure used to optimize the portfolio, by default 'MV'
|
|
Possible values are:
|
|
|
|
- 'MV': Standard Deviation.
|
|
- 'MAD': Mean Absolute Deviation.
|
|
- 'MSV': Semi Standard Deviation.
|
|
- 'FLPM': First Lower Partial Moment (Omega Ratio).
|
|
- 'SLPM': Second Lower Partial Moment (Sortino Ratio).
|
|
- 'CVaR': Conditional Value at Risk.
|
|
- 'EVaR': Entropic Value at Risk.
|
|
- 'WR': Worst Realization.
|
|
- 'ADD': Average Drawdown of uncompounded cumulative returns.
|
|
- 'UCI': Ulcer Index of uncompounded cumulative returns.
|
|
- 'CDaR': Conditional Drawdown at Risk of uncompounded cumulative returns.
|
|
- 'EDaR': Entropic Drawdown at Risk of uncompounded cumulative returns.
|
|
- 'MDD': Maximum Drawdown of uncompounded cumulative returns.
|
|
|
|
risk_free_rate: float, optional
|
|
Risk free rate, annualized. Used for 'FLPM' and 'SLPM' and Sharpe objective function, by default 0.0
|
|
alpha: float, optional
|
|
Significance level of VaR, CVaR, EDaR, DaR, CDaR, EDaR, Tail Gini of losses, by default 0.05
|
|
n_portfolios: int, optional
|
|
Number of portfolios to simulate, by default 100
|
|
seed: int, optional
|
|
Seed used to generate random portfolios, by default 123
|
|
|
|
Examples
|
|
--------
|
|
>>> from openbb_terminal.sdk import openbb
|
|
>>> d = {
|
|
"SECTOR": {
|
|
"AAPL": "INFORMATION TECHNOLOGY",
|
|
"MSFT": "INFORMATION TECHNOLOGY",
|
|
"AMZN": "CONSUMER DISCRETIONARY",
|
|
},
|
|
"CURRENT_INVESTED_AMOUNT": {
|
|
"AAPL": "100000.0",
|
|
"MSFT": "200000.0",
|
|
"AMZN": "300000.0",
|
|
},
|
|
"CURRENCY": {
|
|
"AAPL": "USD",
|
|
"MSFT": "USD",
|
|
"AMZN": "USD",
|
|
},
|
|
}
|
|
>>> p = openbb.portfolio.po.load(symbols_categories=d)
|
|
>>> openbb.portfolio.po.ef_chart(portfolio_engine=p)
|
|
|
|
>>> from openbb_terminal.sdk import openbb
|
|
>>> p = openbb.portfolio.po.load(symbols_file_path="openbb_terminal/miscellaneous/portfolio_examples/allocation/60_40_Portfolio.xlsx")
|
|
>>> openbb.portfolio.po.ef_chart(portfolio_engine=p)
|
|
"""
|
|
|
|
n_portfolios = get_kwarg("n_portfolios", kwargs)
|
|
freq = get_kwarg("freq", kwargs)
|
|
risk_measure = get_kwarg("risk_measure", kwargs)
|
|
risk_free_rate = get_kwarg("risk_free_rate", kwargs)
|
|
alpha = get_kwarg("alpha", kwargs)
|
|
|
|
# Pop chart args
|
|
tangency = kwargs.pop("tangency", False)
|
|
plot_tickers = kwargs.pop("plot_tickers", False)
|
|
external_axes = kwargs.pop("external_axes", False)
|
|
|
|
frontier, mu, cov, stock_returns, weights, X1, Y1, port = get_ef(
|
|
portfolio_engine,
|
|
**kwargs,
|
|
)
|
|
|
|
risk_free_rate = risk_free_rate / TIME_FACTOR[freq.upper()]
|
|
|
|
_, ax = plt.subplots(
|
|
figsize=plot_autoscale(), dpi=get_current_user().preferences.PLOT_DPI
|
|
)
|
|
|
|
ax = rp.plot_frontier(
|
|
w_frontier=frontier,
|
|
mu=mu,
|
|
cov=cov,
|
|
returns=stock_returns,
|
|
rm=RISK_CHOICES[risk_measure.lower()],
|
|
rf=risk_free_rate,
|
|
alpha=alpha,
|
|
cmap="RdYlBu",
|
|
w=weights,
|
|
label="",
|
|
marker="*",
|
|
s=16,
|
|
c="r",
|
|
t_factor=TIME_FACTOR[freq.upper()],
|
|
ax=ax,
|
|
)
|
|
|
|
# Add risk free line
|
|
if tangency:
|
|
ret_sharpe = (mu @ weights).to_numpy().item() * TIME_FACTOR[freq.upper()]
|
|
risk_sharpe = rp.Sharpe_Risk(
|
|
weights,
|
|
cov=cov,
|
|
returns=stock_returns,
|
|
rm=RISK_CHOICES[risk_measure.lower()],
|
|
rf=risk_free_rate,
|
|
alpha=alpha,
|
|
# a_sim=a_sim,
|
|
# beta=beta,
|
|
# b_sim=b_sim,
|
|
)
|
|
if RISK_CHOICES[risk_measure.lower()] not in [
|
|
"ADD",
|
|
"MDD",
|
|
"CDaR",
|
|
"EDaR",
|
|
"UCI",
|
|
]:
|
|
risk_sharpe = risk_sharpe * TIME_FACTOR[freq.upper()] ** 0.5
|
|
|
|
y = ret_sharpe * 1.5
|
|
b = risk_free_rate * TIME_FACTOR[freq.upper()]
|
|
m = (ret_sharpe - b) / risk_sharpe
|
|
x2 = (y - b) / m
|
|
x = [0, x2]
|
|
y = [b, y]
|
|
line = Line2D(x, y, label="Capital Allocation Line")
|
|
ax.set_xlim(xmin=min(X1) * 0.8)
|
|
ax.add_line(line)
|
|
|
|
ax.plot(X1, Y1, color="b")
|
|
|
|
plot_tickers = True
|
|
if plot_tickers:
|
|
ticker_plot = pd.DataFrame(columns=["ticker", "var"])
|
|
for ticker in port.cov.columns:
|
|
weight_df = pd.DataFrame({"weights": 1}, index=[ticker])
|
|
risk = rp.Sharpe_Risk(
|
|
weight_df,
|
|
cov=port.cov[ticker][ticker],
|
|
returns=stock_returns.loc[:, [ticker]],
|
|
rm=RISK_CHOICES[risk_measure.lower()],
|
|
rf=risk_free_rate,
|
|
alpha=alpha,
|
|
)
|
|
|
|
if RISK_CHOICES[risk_measure.lower()] not in [
|
|
"MDD",
|
|
"ADD",
|
|
"CDaR",
|
|
"EDaR",
|
|
"UCI",
|
|
]:
|
|
risk = risk * TIME_FACTOR[freq.upper()] ** 0.5
|
|
|
|
ticker_plot = ticker_plot.append(
|
|
{"ticker": ticker, "var": risk}, ignore_index=True
|
|
)
|
|
ticker_plot = ticker_plot.set_index("ticker")
|
|
ticker_plot = ticker_plot.merge(
|
|
port.mu.T * TIME_FACTOR[freq.upper()], right_index=True, left_index=True
|
|
)
|
|
ticker_plot = ticker_plot.rename(columns={0: "ret"})
|
|
ax.scatter(ticker_plot["var"], ticker_plot["ret"])
|
|
for row in ticker_plot.iterrows():
|
|
ax.annotate(row[0], (row[1]["var"], row[1]["ret"]))
|
|
ax.set_title(f"Efficient Frontier simulating {n_portfolios} portfolios")
|
|
ax.legend(loc="best", scatterpoints=1)
|
|
theme.style_primary_axis(ax)
|
|
L, b, w, h = ax.get_position().bounds
|
|
ax.set_position([L, b, w * 0.9, h])
|
|
ax1 = ax.get_figure().axes
|
|
ll, bb, ww, hh = ax1[-1].get_position().bounds
|
|
ax1[-1].set_position([ll * 1.02, bb, ww, hh])
|
|
|
|
return theme.visualize_output(force_tight_layout=False, external_axes=external_axes)
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def display_plot(
|
|
portfolio_engine: Optional[PoEngine] = None, chart_type: str = "pie", **kwargs
|
|
):
|
|
"""
|
|
Display efficient frontier
|
|
|
|
Parameters
|
|
----------
|
|
portfolio_engine : PoEngine, optional
|
|
Portfolio optimization engine, by default None
|
|
Use `portfolio.po.load` to load a portfolio engine
|
|
chart_type : str, optional
|
|
Chart type, by default "pie"
|
|
Options are "pie", "hist", "dd" or "rc"
|
|
|
|
Examples
|
|
--------
|
|
>>> from openbb_terminal.sdk import openbb
|
|
>>> d = {
|
|
"SECTOR": {
|
|
"AAPL": "INFORMATION TECHNOLOGY",
|
|
"MSFT": "INFORMATION TECHNOLOGY",
|
|
"AMZN": "CONSUMER DISCRETIONARY",
|
|
},
|
|
"CURRENT_INVESTED_AMOUNT": {
|
|
"AAPL": "100000.0",
|
|
"MSFT": "200000.0",
|
|
"AMZN": "300000.0",
|
|
},
|
|
"CURRENCY": {
|
|
"AAPL": "USD",
|
|
"MSFT": "USD",
|
|
"AMZN": "USD",
|
|
},
|
|
}
|
|
>>> p = openbb.portfolio.po.load(symbols_categories=d)
|
|
>>> weights, performance = openbb.portfolio.po.equal(portfolio_engine=p)
|
|
>>> p.get_available_categories()
|
|
['SECTOR', 'CURRENCY']
|
|
>>> openbb.portfolio.po.plot(portfolio_engine=p, category="SECTOR", chart_type="pie")
|
|
>>> openbb.portfolio.po.plot(portfolio_engine=p, category="SECTOR", chart_type="hist")
|
|
>>> openbb.portfolio.po.plot(portfolio_engine=p, category="SECTOR", chart_type="dd")
|
|
>>> openbb.portfolio.po.plot(portfolio_engine=p, category="SECTOR", chart_type="rc")
|
|
>>> openbb.portfolio.po.plot(portfolio_engine=p, category="SECTOR", chart_type="heat")
|
|
|
|
>>> from openbb_terminal.sdk import openbb
|
|
>>> p = openbb.portfolio.po.load(symbols_file_path="openbb_terminal/miscellaneous/portfolio_examples/allocation/60_40_Portfolio.xlsx")
|
|
>>> weights, performance = openbb.portfolio.po.equal(portfolio_engine=p)
|
|
>>> p.get_available_categories()
|
|
['ASSET_CLASS',
|
|
'SECTOR',
|
|
'INDUSTRY',
|
|
'COUNTRY',
|
|
'CURRENCY']
|
|
>>> openbb.portfolio.po.plot(portfolio_engine=p, category="ASSET_CLASS", chart_type="pie")
|
|
>>> openbb.portfolio.po.plot(portfolio_engine=p, category="SECTOR", chart_type="hist")
|
|
>>> openbb.portfolio.po.plot(portfolio_engine=p, category="INDUSTRY", chart_type="dd")
|
|
>>> openbb.portfolio.po.plot(portfolio_engine=p, category="COUNTRY", chart_type="rc")
|
|
>>> openbb.portfolio.po.plot(portfolio_engine=p, category="ASSET_CLASS", chart_type="heat")
|
|
"""
|
|
|
|
if portfolio_engine is None:
|
|
console.print("No portfolio engine found.")
|
|
return
|
|
|
|
available_categories = portfolio_engine.get_available_categories()
|
|
|
|
if not available_categories:
|
|
console.print("No categories found.")
|
|
return
|
|
msg = ", ".join(available_categories)
|
|
if "category" not in kwargs:
|
|
console.print(f"Please specify a category from the following: {msg}")
|
|
return
|
|
if kwargs["category"] not in available_categories:
|
|
console.print(f"Please specify a category from the following: {msg}")
|
|
return
|
|
|
|
category = kwargs["category"]
|
|
|
|
_, valid_portfolio_engine, valid_kwargs = validate_inputs(
|
|
portfolio_engine=portfolio_engine, kwargs=kwargs
|
|
)
|
|
|
|
weights: pd.DataFrame = valid_portfolio_engine.get_weights_df()
|
|
if weights.empty:
|
|
return
|
|
|
|
data: pd.DataFrame = valid_portfolio_engine.get_returns()
|
|
if data.empty:
|
|
return
|
|
|
|
if category:
|
|
category_dict = valid_portfolio_engine.get_category(category)
|
|
category_df = pd.DataFrame.from_dict(
|
|
data=category_dict, orient="index", columns=["category"]
|
|
)
|
|
weights = weights.join(category_df, how="inner")
|
|
weights.sort_index(inplace=True)
|
|
|
|
# Calculating classes returns
|
|
classes = list(set(weights["category"]))
|
|
weights_classes = weights.groupby(["category"]).sum()
|
|
matrix_classes = np.zeros((len(weights), len(classes)))
|
|
labels = weights["category"].tolist()
|
|
|
|
j_value = 0
|
|
for i in classes:
|
|
matrix_classes[:, j_value] = np.array(
|
|
[1 if x == i else 0 for x in labels], dtype=float
|
|
)
|
|
matrix_classes[:, j_value] = (
|
|
matrix_classes[:, j_value]
|
|
* weights["value"]
|
|
/ weights_classes.loc[i, "value"]
|
|
)
|
|
j_value += 1
|
|
|
|
matrix_classes = pd.DataFrame(
|
|
matrix_classes, columns=classes, index=weights.index
|
|
)
|
|
data = data @ matrix_classes
|
|
weights = weights_classes["value"].copy()
|
|
weights.replace(0, np.nan, inplace=True)
|
|
weights.dropna(inplace=True)
|
|
weights.sort_values(ascending=True, inplace=True)
|
|
data = data[weights.index.tolist()]
|
|
data.columns = [i.title() for i in data.columns]
|
|
weights.index = [i.title() for i in weights.index]
|
|
weights = weights.to_dict()
|
|
|
|
valid_kwargs["weights"] = weights
|
|
valid_kwargs["data"] = data
|
|
valid_kwargs["colors"] = theme.get_colors()
|
|
|
|
if chart_type == "pie":
|
|
display_pie(**valid_kwargs)
|
|
elif chart_type == "hist":
|
|
display_hist(**valid_kwargs)
|
|
elif chart_type == "dd":
|
|
display_dd(**valid_kwargs)
|
|
elif chart_type == "rc":
|
|
display_rc(**valid_kwargs)
|
|
elif chart_type == "heat":
|
|
display_heat(**valid_kwargs)
|
|
else:
|
|
console.print(
|
|
"Invalid chart type, please choose from the following: pie, hist, dd, rc, heat"
|
|
)
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def display_heat(**kwargs):
|
|
weights = kwargs.get("weights", None)
|
|
data = kwargs.get("data", None)
|
|
category = kwargs.get("category", None)
|
|
title = kwargs.get("title", "")
|
|
external_axes = kwargs.get("external_axes", False)
|
|
|
|
if len(weights) == 1:
|
|
single_key = list(weights.keys())[0].upper()
|
|
console.print(
|
|
f"[yellow]Heatmap needs at least two values for '{category}', only found '{single_key}'.[/yellow]"
|
|
)
|
|
return None
|
|
|
|
_, ax = plt.subplots(
|
|
figsize=plot_autoscale(), dpi=get_current_user().preferences.PLOT_DPI
|
|
)
|
|
|
|
number_of_clusters = len(weights) if len(weights) <= 3 else None
|
|
|
|
ax = rp.plot_clusters(
|
|
returns=data,
|
|
codependence="pearson",
|
|
linkage="ward",
|
|
k=number_of_clusters,
|
|
max_k=10,
|
|
leaf_order=True,
|
|
dendrogram=True,
|
|
cmap="RdYlBu",
|
|
# linecolor='tab:purple',
|
|
ax=ax,
|
|
)
|
|
|
|
ax = ax.get_figure().axes
|
|
ax[0].grid(False)
|
|
ax[0].axis("off")
|
|
|
|
if category is None:
|
|
# Vertical dendrogram
|
|
L, b, w, h = ax[4].get_position().bounds
|
|
l1 = L * 0.5
|
|
w1 = w * 0.2
|
|
b1 = h * 0.05
|
|
ax[4].set_position([L - l1, b + b1, w * 0.8, h * 0.95])
|
|
# Heatmap
|
|
L, b, w, h = ax[1].get_position().bounds
|
|
ax[1].set_position([L - l1 - w1, b + b1, w * 0.8, h * 0.95])
|
|
w2 = w * 0.2
|
|
# colorbar
|
|
L, b, w, h = ax[2].get_position().bounds
|
|
ax[2].set_position([L - l1 - w1 - w2, b, w, h])
|
|
# Horizontal dendrogram
|
|
L, b, w, h = ax[3].get_position().bounds
|
|
ax[3].set_position([L - l1 - w1, b, w * 0.8, h])
|
|
else:
|
|
# Vertical dendrogram
|
|
L, b, w, h = ax[4].get_position().bounds
|
|
l1 = L * 0.5
|
|
w1 = w * 0.4
|
|
b1 = h * 0.2
|
|
ax[4].set_position([L - l1, b + b1, w * 0.6, h * 0.8])
|
|
# Heatmap
|
|
L, b, w, h = ax[1].get_position().bounds
|
|
ax[1].set_position([L - l1 - w1, b + b1, w * 0.6, h * 0.8])
|
|
w2 = w * 0.05
|
|
# colorbar
|
|
L, b, w, h = ax[2].get_position().bounds
|
|
ax[2].set_position([L - l1 - w1 - w2, b, w, h])
|
|
# Horizontal dendrogram
|
|
L, b, w, h = ax[3].get_position().bounds
|
|
ax[3].set_position([L - l1 - w1, b, w * 0.6, h])
|
|
|
|
title = "Portfolio - " + title + "\n"
|
|
title += ax[3].get_title(loc="left")
|
|
ax[3].set_title(title)
|
|
|
|
return theme.visualize_output(force_tight_layout=True, external_axes=external_axes)
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def display_rc(**kwargs):
|
|
weights = kwargs.get("weights", None)
|
|
data = kwargs.get("data", None)
|
|
colors = kwargs.get("colors", None)
|
|
title = kwargs.get("title", "")
|
|
external_axes = kwargs.get("external_axes", False)
|
|
|
|
risk_measure = get_kwarg("risk_measure", kwargs)
|
|
risk_free_rate = get_kwarg("risk_free_rate", kwargs)
|
|
alpha = get_kwarg("alpha", kwargs)
|
|
a_sim = get_kwarg("a_sim", kwargs)
|
|
beta = get_kwarg("beta", kwargs)
|
|
b_sim = get_kwarg("b_sim", kwargs)
|
|
freq = get_kwarg("freq", kwargs)
|
|
|
|
_, ax = plt.subplots(
|
|
figsize=plot_autoscale(), dpi=get_current_user().preferences.PLOT_DPI
|
|
)
|
|
|
|
ax = rp.plot_risk_con(
|
|
w=pd.Series(weights).to_frame(),
|
|
cov=data.cov(),
|
|
returns=data,
|
|
rm=RISK_CHOICES[risk_measure.lower()],
|
|
rf=risk_free_rate,
|
|
alpha=alpha,
|
|
a_sim=a_sim,
|
|
beta=beta,
|
|
b_sim=b_sim,
|
|
color=colors[1],
|
|
t_factor=TIME_FACTOR[freq.upper()],
|
|
ax=ax,
|
|
)
|
|
|
|
# Changing colors
|
|
for i in ax.get_children()[:-1]:
|
|
if isinstance(i, matplotlib.patches.Rectangle):
|
|
i.set_width(i.get_width())
|
|
i.set_color(colors[0])
|
|
|
|
title = "Portfolio - " + title + "\n"
|
|
title += ax.get_title(loc="left")
|
|
ax.set_title(title)
|
|
|
|
return theme.visualize_output(force_tight_layout=True, external_axes=external_axes)
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def display_hist(**kwargs):
|
|
weights = kwargs.get("weights", None)
|
|
data = kwargs.get("data", None)
|
|
colors = kwargs.get("colors", None)
|
|
title = kwargs.get("title", "")
|
|
external_axes = kwargs.get("external_axes", False)
|
|
|
|
alpha = kwargs.get("alpha", 0.05)
|
|
|
|
_, ax = plt.subplots(
|
|
figsize=plot_autoscale(), dpi=get_current_user().preferences.PLOT_DPI
|
|
)
|
|
|
|
ax = rp.plot_hist(data, w=pd.Series(weights).to_frame(), alpha=alpha, ax=ax)
|
|
ax.legend(fontsize="x-small", loc="best")
|
|
|
|
# Changing colors
|
|
for i in ax.get_children()[:-1]:
|
|
if isinstance(i, matplotlib.patches.Rectangle):
|
|
i.set_color(colors[0])
|
|
i.set_alpha(0.7)
|
|
|
|
k = 1
|
|
for i, j in zip(ax.get_legend().get_lines()[::-1], ax.get_lines()[::-1]):
|
|
i.set_color(colors[k])
|
|
j.set_color(colors[k])
|
|
k += 1
|
|
|
|
title = "Portfolio - " + title + "\n"
|
|
title += ax.get_title(loc="left")
|
|
ax.set_title(title)
|
|
|
|
return theme.visualize_output(force_tight_layout=True, external_axes=external_axes)
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def display_dd(**kwargs):
|
|
weights = kwargs.get("weights", None)
|
|
data = kwargs.get("data", None)
|
|
colors = kwargs.get("colors", None)
|
|
title = kwargs.get("title", "")
|
|
external_axes = kwargs.get("external_axes", False)
|
|
|
|
alpha = get_kwarg("alpha", kwargs)
|
|
|
|
_, ax = plt.subplots(
|
|
figsize=plot_autoscale(), dpi=get_current_user().preferences.PLOT_DPI
|
|
)
|
|
|
|
nav = data.cumsum()
|
|
ax = rp.plot_drawdown(nav=nav, w=pd.Series(weights).to_frame(), alpha=alpha, ax=ax)
|
|
|
|
ax[0].remove()
|
|
ax = ax[1]
|
|
fig = ax.get_figure()
|
|
gs = GridSpec(1, 1, figure=fig)
|
|
ax.set_position(gs[0].get_position(fig))
|
|
ax.set_subplotspec(gs[0])
|
|
|
|
# Changing colors
|
|
ax.get_lines()[0].set_color(colors[0])
|
|
k = 1
|
|
for i, j in zip(ax.get_legend().get_lines()[::-1], ax.get_lines()[1:][::-1]):
|
|
i.set_color(colors[k])
|
|
j.set_color(colors[k])
|
|
k += 1
|
|
|
|
ax.get_children()[1].set_facecolor(colors[0])
|
|
ax.get_children()[1].set_alpha(0.7)
|
|
|
|
title = "Portfolio - " + title + "\n"
|
|
title += ax.get_title(loc="left")
|
|
ax.set_title(title)
|
|
|
|
return theme.visualize_output(force_tight_layout=True, external_axes=external_axes)
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def display_pie(**kwargs):
|
|
"""Show a pie chart of holdings
|
|
|
|
Parameters
|
|
----------
|
|
weights: dict
|
|
Weights to display, where keys are tickers, and values are either weights or values if -v specified
|
|
title: str
|
|
Title to be used on the plot title
|
|
external_axes:Optional[List[plt.Axes]]
|
|
Optional external axes to plot data on
|
|
"""
|
|
|
|
weights = kwargs.get("weights", None)
|
|
colors = kwargs.get("colors", None)
|
|
title = kwargs.get("title", "")
|
|
external_axes = kwargs.get("external_axes", False)
|
|
|
|
if not weights:
|
|
return None
|
|
|
|
init_stocks = list(weights.keys())
|
|
init_sizes = list(weights.values())
|
|
symbols = []
|
|
sizes = []
|
|
for stock, size in zip(init_stocks, init_sizes):
|
|
if size > 0:
|
|
symbols.append(stock)
|
|
sizes.append(size)
|
|
|
|
total_size = np.sum(sizes)
|
|
colors = theme.get_colors()
|
|
|
|
_, ax = plt.subplots(
|
|
figsize=plot_autoscale(), dpi=get_current_user().preferences.PLOT_DPI
|
|
)
|
|
|
|
if math.isclose(sum(sizes), 1, rel_tol=0.1):
|
|
_, _, autotexts = ax.pie(
|
|
sizes,
|
|
labels=symbols,
|
|
autopct=my_autopct,
|
|
colors=colors,
|
|
textprops=dict(color="white"),
|
|
wedgeprops={"linewidth": 0.5, "edgecolor": "white"},
|
|
labeldistance=1.05,
|
|
startangle=45,
|
|
normalize=True,
|
|
)
|
|
plt.setp(autotexts, color="white", fontweight="bold")
|
|
else:
|
|
_, _, autotexts = ax.pie(
|
|
sizes,
|
|
labels=symbols,
|
|
autopct="",
|
|
colors=colors,
|
|
textprops=dict(color="white"),
|
|
wedgeprops={"linewidth": 0.5, "edgecolor": "white"},
|
|
labeldistance=1.05,
|
|
startangle=45,
|
|
normalize=True,
|
|
)
|
|
plt.setp(autotexts, color="white", fontweight="bold")
|
|
for i, a in enumerate(autotexts):
|
|
if sizes[i] / total_size > 0.05:
|
|
a.set_text(f"{sizes[i]:.2f}")
|
|
else:
|
|
a.set_text("")
|
|
|
|
ax.axis("equal")
|
|
|
|
# leg1 = ax.legend(
|
|
# wedges,
|
|
# [str(s) for s in stocks],
|
|
# title=" Ticker",
|
|
# loc="upper left",
|
|
# bbox_to_anchor=(0.80, 0, 0.5, 1),
|
|
# frameon=False,
|
|
# )
|
|
# leg2 = ax.legend(
|
|
# wedges,
|
|
# [
|
|
# f"{' ' if ((100*s/total_size) < 10) else ''}{100*s/total_size:.2f}%"
|
|
# for s in sizes
|
|
# ],
|
|
# title=" ",
|
|
# loc="upper left",
|
|
# handlelength=0,
|
|
# bbox_to_anchor=(0.91, 0, 0.5, 1),
|
|
# frameon=False,
|
|
# )
|
|
# ax.add_artist(leg1)
|
|
# ax.add_artist(leg2)
|
|
|
|
plt.setp(autotexts, size=8, weight="bold")
|
|
|
|
title = "Portfolio - " + title + "\n"
|
|
title += "Portfolio Composition"
|
|
ax.set_title(title)
|
|
|
|
return theme.visualize_output(force_tight_layout=True, external_axes=external_axes)
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def my_autopct(x):
|
|
"""Function for autopct of plt.pie. This results in values not being printed in the pie if they are 'too small'"""
|
|
if x > 4:
|
|
return f"{x:.2f} %"
|
|
|
|
return ""
|