From f33dddebc4b72312d716cd1e915cd27719c5917c Mon Sep 17 00:00:00 2001 From: Jamie Date: Wed, 14 Apr 2021 11:56:56 -0400 Subject: [PATCH] more refactoring + edit pie chart function --- .../portfolio_optimization/README.md | 35 +++++++- .../optimizer_helper.py | 90 ++++++++++++++----- .../portfolio_optimization/optimizer_view.py | 14 +-- .../portfolio_optimization/po_controller.py | 2 +- 4 files changed, 111 insertions(+), 30 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md index c5fb0c4fe6f..f0f1b62fc5b 100644 --- a/gamestonk_terminal/portfolio_optimization/README.md +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -117,7 +117,7 @@ The usage is: eff_risk [-p PERIOD] [-r --risk RISK_LEVEL] [-v --value VALUE] [--pie] ```` * -p/--period Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo. -* -r/--risk Risk tolerance. 5% is 0.05. +* -r/--risk Risk tolerance. Default is 0.1 (10%) * -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. * --pie Flag that displays a pie chart of the allocations. @@ -128,15 +128,42 @@ The usage is: eff_ret [-p PERIOD] [-r --return] [-v --value VALUE] [--pie] ```` * -p/--period Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo. -* -r/--return. Desired return. 5% is 0.05. +* -r/--return. Desired return. Default is 0.1 (10%) * -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. * --pie Flag that displays a pie chart of the allocations. ### show_eff -This function plots random portfolios basd on their risk and returns and shows the efficient frontier. +This function plots random portfolios based on their risk and returns and shows the efficient frontier. The usage is: ```` show_eff [-p PERIOD] [-n N_PORTFOLIOS] ```` * -p/--period Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo. -* -n Number of portfolios to simulate. \ No newline at end of file +* -n Number of portfolios to simulate. + +##Sample Usage +In this example, we generate weights for a list of 6 stocks using the eff_ret command. This optimization looks to maximize returns +at a given risk level. We start by adding the stocks we want to analyze: +```` +select aapl,amzn,msft,f,gm,ge +```` +Which shows: +```` +Current Tickers: GE, GM, AMZN, AAPL, F, MSFT +```` +To perform the optimization, we will set a target return of 25% (.25). Given how stocks performed during COVID, +there is a lot of volatility, so many optimizations may not get low volatility. The module also returns annualized volatility, +so the number is your portfolio volatility * `sqrt(252`. Ths optimization will be (including a pie chart!). Note we could also supply a different +time period, which changes the expected returns and historical volatility, which changes the optimization. We could also specify a dollar +amount that you wish to allocate using the `-v` flag. +```` +eff_ret -r .25 --pie +```` +The console will show (numbers will vary based on when this is done). +![console](https://user-images.githubusercontent.com/18151143/114740311-bd429c80-9d17-11eb-90e2-97430781431a.png) + +And the pie chart: + +![yummypie](https://user-images.githubusercontent.com/18151143/114740289-b9167f00-9d17-11eb-9c29-470785b21d09.png) + +Note that since `AAPL` had zero allocation, it was ommitted from the chart. \ No newline at end of file diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py index 1215e34316b..e341f32ae24 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py @@ -2,9 +2,10 @@ __docformat__ = "numpy" import argparse -from typing import List +from typing import List, Optional import math import pandas as pd +import numpy as np import matplotlib.pyplot as plt import yfinance as yf from pypfopt.efficient_frontier import EfficientFrontier @@ -14,7 +15,15 @@ from gamestonk_terminal.config_plot import PLOT_DPI from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import plot_autoscale, parse_known_args_and_warn -title_maps = {} +title_maps = { + "max_sharpe": "Maximum Sharpe Portfolio", + "min_volatility": "Minimum Volatility Portfolio", + "eff_risk": "Maximum Return Portfolio at Risk = {:1f} %", + "eff_ret": "Minimum Volatility Portfolio at Target Return = {:.1f} %", + "equal": "Equally Weighted Portfolio", + "marketCap": "MarketCap Weighted Portfolio", + "dividendYield": "Dividend Yield Weighted Portfolio", +} def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFrame: @@ -71,11 +80,24 @@ def display_weights(weights: dict): if not weights: return weight_df = pd.DataFrame.from_dict(data=weights, orient="index", columns=["value"]) - weight_df["weight"] = (weight_df["value"] * 100).astype(str) + " %" - print(weight_df["weight"]) + 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] + ) + " %" + print(pd.DataFrame(weight_df["weight"])) + else: + print(weight_df) -def pie_chart_weights(weights: dict, optimizer: str): +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} %" + else: + return "" + + +def pie_chart_weights(weights: dict, optimizer: str, value: Optional[float]): """ Show a pie chart of holdings Parameters @@ -83,43 +105,71 @@ def pie_chart_weights(weights: dict, optimizer: str): weights: dict weights to display. Keys are stocks. Values are either weights or values if -v specified optimzer: str - Optmization technique used + Optimization technique used for title """ - + plt.close("all") if not weights: return - stocks = list(weights.keys()) - sizes = list(weights.values()) - _, ax = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI) + stocks = np.array(list(weights.keys())) + sizes = np.array(list(weights.values())) + + to_not_include = sizes == 0 + + stocks, sizes = stocks[to_not_include == False], sizes[to_not_include == False] + 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) + ] + if math.isclose(sum(sizes), 1, rel_tol=0.1): - wedges, _, autotexts = ax.pie( + wedges, _, autotexts = plt.pie( sizes, labels=stocks, - autopct="%1.1f%%", + autopct=my_autopct, textprops=dict(color="k"), - labeldistance=5, + explode=[s / (5 * total_size) for s in sizes], + normalize=True, + shadow=True, ) else: - wedges, _, autotexts = ax.pie( - sizes, labels=stocks, autopct="", textprops=dict(color="k"), labeldistance=5 + wedges, _, autotexts = plt.pie( + sizes, + labels=stocks, + autopct="", + textprops=dict(color="k"), + explode=[s / (5 * total_size) for s in sizes], + normalize=True, + shadow=True, ) for i, a in enumerate(autotexts): - a.set_text(f"{sizes[i]}") + if sizes[i] / total_size > 0.05: + a.set_text(f"{sizes[i]:.2f}") + else: + a.set_text("") - ax.axis("equal") - ax.legend( + plt.axis("equal") + + plt.legend( wedges, - stocks, + leg_labels, title="Stocks", loc="center left", bbox_to_anchor=(0.85, 0, 0.5, 1), ) plt.setp(autotexts, size=8, weight="bold") - ax.set_title("Portfolio Holdings") + + if optimizer in ["eff_ret", "eff_risk"]: + plt.title(title_maps[optimizer].format(100 * value)) + else: + plt.title(title_maps[optimizer]) + if gtff.USE_ION: plt.ion() + plt.show() print("") diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py index bad19006ac8..75f24bef38e 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -81,7 +81,7 @@ def equal_weight(stocks: List[str], other_args: List[str]): for stock in stocks: values[stock] = ns_parser.value * round(1 / n_stocks, 5) if ns_parser.pie: - pie_chart_weights(values) + pie_chart_weights(values, "equal", None) if n_stocks >= 1: print("Equal Weight Portfolio: ") display_weights(values) @@ -147,7 +147,7 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s weights[k] = round(v / prop_sum, 5) * ns_parser.value if ns_parser.pie: - pie_chart_weights(weights) + pie_chart_weights(weights, property_type, None) if property_type == "marketCap": print("Market Cap Weighted Portfolio: ") @@ -225,6 +225,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): key: ns_parser.value * round(value, 5) for key, value in ef_sharpe.items() } + val = None print("Weights that maximize Sharpe Ratio:") elif port_type == "min_volatility": @@ -234,6 +235,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): key: ns_parser.value * round(value, 5) for key, value in ef_min_vol.items() } + val = None print("Weights that minimize volatility") elif port_type == "eff_risk": @@ -243,7 +245,8 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): key: ns_parser.value * round(value, 5) for key, value in ef_eff_risk.items() } - print("Weights for maximizing returns at your risk level:") + val = ns_parser.risk_level + print(f"Weights for maximizing returns at risk = {100*val:.1f} %") elif port_type == "eff_ret": @@ -252,13 +255,14 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): key: ns_parser.value * round(value, 5) for key, value in ef_eff_risk.items() } - print("Weights for minimizing risk at your return:") + val = ns_parser.target_return + print(f"Weights for minimizing risk at target return = {100*val:.1f} %") else: raise ValueError("EF Method not found") if ns_parser.pie: - pie_chart_weights(weights, port_type) + pie_chart_weights(weights, port_type, val) print("") ef.portfolio_performance(verbose=True) diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index fa515d0811b..df2f8a55fa6 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -10,7 +10,6 @@ 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.optimizer_helper import display_weights class PortfolioOptimization: @@ -179,6 +178,7 @@ def menu(tickers: List[str]): """Portfolio Optimization Menu""" if tickers == [""]: tickers = [] + plt.close("all") po_controller = PortfolioOptimization(tickers) po_controller.call_help(tickers)