diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py index 3ca81f396a2..24db45eed91 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py @@ -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("") diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py index e9d74a76138..6e779b0669a 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -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) diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index df2f8a55fa6..d4d2960adee 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -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: