Improve portfolio optimization user experience - pedantic stuff

This commit is contained in:
didier
2021-04-15 22:12:45 +01:00
parent 5904c148f8
commit 4f9669684b
3 changed files with 98 additions and 77 deletions

View File

@@ -27,7 +27,7 @@ title_maps = {
def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFrame:
"""
"""Get adjusted closing price for each stock in the list
Parameters
----------
@@ -51,8 +51,8 @@ def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFra
def prepare_efficient_frontier(stock_prices: pd.DataFrame):
"""
Take in a dataframe of prices and return an efficient frontier object
"""Take in a dataframe of prices and return an efficient frontier object
Parameters
----------
stock_prices : DataFrame
@@ -82,11 +82,11 @@ def display_weights(weights: dict):
weight_df = pd.DataFrame.from_dict(data=weights, orient="index", columns=["value"])
if math.isclose(weight_df.sum()["value"], 1, rel_tol=0.1):
weight_df["weight"] = (weight_df["value"] * 100).astype(str).apply(
lambda s: s[:6]
lambda s: " " + s[:4] if s.find(".") == 1 else "" + s[:5]
) + " %"
print(pd.DataFrame(weight_df["weight"]))
print(pd.DataFrame(weight_df["weight"]).to_string(header=False))
else:
print(weight_df)
print(weight_df.to_string(header=False))
def my_autopct(x):
@@ -107,7 +107,6 @@ def pie_chart_weights(weights: dict, optimizer: str, value: float):
optimzer: str
Optimization technique used for title
"""
plt.close("all")
if not weights:
return
@@ -122,31 +121,28 @@ def pie_chart_weights(weights: dict, optimizer: str, value: float):
total_size = np.sum(sizes)
leg_labels = [
f"{str(a)}: {str(round(100*b/total_size,3))[:4]}%"
for a, b in zip(stocks, sizes)
]
plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI)
if math.isclose(sum(sizes), 1, rel_tol=0.1):
wedges, _, autotexts = plt.pie(
sizes,
labels=stocks,
autopct=my_autopct,
textprops=dict(color="k"),
explode=[s / (5 * total_size) for s in sizes],
wedgeprops={"linewidth": 3.0, "edgecolor": "white"},
normalize=True,
shadow=True,
)
plt.setp(autotexts, color="white", fontweight="bold")
else:
wedges, _, autotexts = plt.pie(
sizes,
labels=stocks,
autopct="",
textprops=dict(color="k"),
explode=[s / (5 * total_size) for s in sizes],
wedgeprops={"linewidth": 3.0, "edgecolor": "white"},
normalize=True,
shadow=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}")
@@ -155,24 +151,41 @@ def pie_chart_weights(weights: dict, optimizer: str, value: float):
plt.axis("equal")
plt.legend(
leg1 = plt.legend(
wedges,
leg_labels,
title="Stocks",
loc="center left",
bbox_to_anchor=(0.85, 0, 0.5, 1),
[str(s) for s in stocks],
title=" Ticker",
loc="upper left",
bbox_to_anchor=(0.80, 0, 0.5, 1),
frameon=False,
)
leg2 = plt.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,
)
plt.gca().add_artist(leg1)
plt.gca().add_artist(leg2)
plt.setp(autotexts, size=8, weight="bold")
if optimizer in ["eff_ret", "eff_risk"]:
plt.title(title_maps[optimizer].format(100 * value))
plt.gca().set_title(title_maps[optimizer].format(100 * value), pad=20)
else:
plt.title(title_maps[optimizer])
plt.gca().set_title(title_maps[optimizer], pad=20)
if gtff.USE_ION:
plt.ion()
plt.tight_layout()
plt.show()
print("")

View File

@@ -76,14 +76,15 @@ def equal_weight(stocks: List[str], other_args: List[str]):
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return
if len(stocks) < 2:
print("Please have at least 2 loaded tickers to calculate weights.\n")
return
values = {}
n_stocks = len(stocks)
for stock in stocks:
values[stock] = ns_parser.value * round(1 / n_stocks, 5)
values[stock] = ns_parser.value * round(1 / len(stocks), 5)
if ns_parser.pie:
pie_chart_weights(values, "equal", 0)
if n_stocks >= 1:
print("Equal Weight Portfolio: ")
else:
display_weights(values)
print("")
@@ -137,6 +138,9 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return
if len(stocks) < 2:
print("Please have at least 2 loaded tickers to calculate weights.\n")
return
for stock in stocks:
stock_prop = yf.Ticker(stock).info[property_type]
if stock_prop is None:
@@ -148,14 +152,7 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s
if ns_parser.pie:
pie_chart_weights(weights, property_type, 0)
if property_type == "marketCap":
print("Market Cap Weighted Portfolio: ")
elif property_type == "dividendYield":
print("Dividend Yield Weighted Portfolio: ")
if len(stocks) >= 1:
else:
display_weights(weights)
print("")
@@ -296,7 +293,7 @@ def show_ef(stocks: List[str], other_args: List[str]):
if not ns_parser:
return
if len(stocks) < 2:
print("Please have at least 2 loaded tickers to calculate weights.\n")
print("Please have at least 2 loaded tickers to calculate weights.")
return
stock_prices = process_stocks(stocks, ns_parser.period)

View File

@@ -9,7 +9,7 @@ from prompt_toolkit.completion import NestedCompleter
from gamestonk_terminal import feature_flags as gtff
from gamestonk_terminal.helper_funcs import get_flair, parse_known_args_and_warn
from gamestonk_terminal.menu import session
from gamestonk_terminal.portfolio_optimization import optimizer_view as po_api
from gamestonk_terminal.portfolio_optimization import optimizer_view
class PortfolioOptimization:
@@ -20,14 +20,14 @@ class PortfolioOptimization:
"quit",
"select",
"add",
"equal_weight",
"mkt_cap",
"div_yield",
"max_sharpe",
"min_vol",
"eff_risk",
"eff_ret",
"show_ef",
"equal",
"mktcap",
"dividend",
"maxsharpe",
"minvol",
"maxret",
"minrisk",
"ef",
"yolo",
]
@@ -52,21 +52,19 @@ class PortfolioOptimization:
print(" quit quit to abandon program")
print(f"\nCurrent Tickers: {('None', ', '.join(tickers))[bool(tickers)]}")
print("")
print(" add add ticker to optimize")
print(" select overwrite current tickers with new tickers")
print(" add add ticker to optimize")
print(" select overwrite current tickers with new tickers")
print("")
print("Optimization:")
print(" equal equally weighted")
print(" mktcap market cap weighted")
print(" dividend dividend weighted")
print(" maxsharpe maximum sharpe ratio")
print(" minvol minimum volatility")
print(" maxret maximizes returns at given risk")
print(" minrisk minimizes risk at given return")
print(" ef show the efficient frontier")
print("")
print(" equal_weight equally weighted portfolio")
print(" mkt_cap marketcap weighted portfolio")
print(" div_yield dividend weighted portfolio")
print(" max_sharpe portfolio with maximum sharpe ratio")
print(" min_vol portfolio with minimum volatility")
print(" eff_risk portfolio that maximizes returns at given risk")
print(" eff_ret portfolio that minimizes risk at given return")
print(" show_ef show the efficient frontier")
print("")
plt.close("all")
def switch(self, an_input: str):
"""Process and dispatch input
@@ -97,36 +95,45 @@ class PortfolioOptimization:
return True
def call_add(self, other_args: List[str]):
"""Process add command"""
self.add_stocks(self, other_args)
def call_select(self, other_args: List[str]):
"""Process select command"""
self.tickers = []
self.add_stocks(self, other_args)
def call_equal_weight(self, other_args: List[str]):
po_api.equal_weight(self.tickers, other_args)
def call_equal(self, other_args: List[str]):
"""Process equal command"""
optimizer_view.equal_weight(self.tickers, other_args)
def call_mkt_cap(self, other_args: List[str]):
po_api.property_weighting(self.tickers, "marketCap", other_args)
def call_mktcap(self, other_args: List[str]):
"""Process mktcap command"""
optimizer_view.property_weighting(self.tickers, "marketCap", other_args)
def call_div_yield(self, other_args: List[str]):
po_api.property_weighting(self.tickers, "dividendYield", other_args)
def call_dividend(self, other_args: List[str]):
"""Process dividend command"""
optimizer_view.property_weighting(self.tickers, "dividendYield", other_args)
def call_max_sharpe(self, other_args: List[str]):
po_api.ef_portfolio(self.tickers, "max_sharpe", other_args)
def call_maxsharpe(self, other_args: List[str]):
"""Process maxsharpe command"""
optimizer_view.ef_portfolio(self.tickers, "max_sharpe", other_args)
def call_min_vol(self, other_args: List[str]):
po_api.ef_portfolio(self.tickers, "min_volatility", other_args)
def call_minvol(self, other_args: List[str]):
"""Process minvol command"""
optimizer_view.ef_portfolio(self.tickers, "min_volatility", other_args)
def call_eff_risk(self, other_args: List[str]):
po_api.ef_portfolio(self.tickers, "eff_risk", other_args)
def call_maxret(self, other_args: List[str]):
"""Process maxret command"""
optimizer_view.ef_portfolio(self.tickers, "eff_risk", other_args)
def call_eff_ret(self, other_args: List[str]):
po_api.ef_portfolio(self.tickers, "eff_ret", other_args)
def call_minrisk(self, other_args: List[str]):
"""Process minrisk command"""
optimizer_view.ef_portfolio(self.tickers, "eff_ret", other_args)
def call_show_ef(self, other_args):
po_api.show_ef(self.tickers, other_args)
print("")
def call_ef(self, other_args):
"""Process ef command"""
optimizer_view.show_ef(self.tickers, other_args)
def call_yolo(self, _):
# Easter egg :)
@@ -162,12 +169,14 @@ class PortfolioOptimization:
tickers = set(self.tickers)
for ticker in ns_parser.add_tickers:
tickers.add(ticker)
if self.tickers:
print(
f"\nCurrent Tickers: {('None', ', '.join(tickers))[bool(tickers)]}"
)
self.tickers = list(tickers)
print(
f"\nCurrent Tickers: {('None', ', '.join(self.tickers))[bool(self.tickers)]}"
)
print("")
except Exception as e:
print(e)
@@ -196,6 +205,8 @@ def menu(tickers: List[str]):
an_input = input(f"{get_flair()} (po)> ")
try:
plt.close("all")
process_input = po_controller.switch(an_input)
if process_input is not None: