From aeaf6b4e16c8b52bfd0b98c1ba1d055bfa003c06 Mon Sep 17 00:00:00 2001 From: Jamie Date: Tue, 13 Apr 2021 17:25:19 -0400 Subject: [PATCH] refactoring :) --- ...port_opt_helper.py => optimizer_helper.py} | 73 ++++- .../{port_opt_api.py => optimizer_view.py} | 305 ++++++++---------- .../portfolio_optimization/po_controller.py | 39 +-- 3 files changed, 215 insertions(+), 202 deletions(-) rename gamestonk_terminal/portfolio_optimization/{port_opt_helper.py => optimizer_helper.py} (59%) rename gamestonk_terminal/portfolio_optimization/{port_opt_api.py => optimizer_view.py} (65%) diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_helper.py b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py similarity index 59% rename from gamestonk_terminal/portfolio_optimization/port_opt_helper.py rename to gamestonk_terminal/portfolio_optimization/optimizer_helper.py index 809f99ffc12..1215e34316b 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_helper.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py @@ -1,6 +1,7 @@ """ Portfolio Optimization Helper Functions""" __docformat__ = "numpy" +import argparse from typing import List import math import pandas as pd @@ -11,7 +12,9 @@ from pypfopt import risk_models from pypfopt import expected_returns from gamestonk_terminal.config_plot import PLOT_DPI from gamestonk_terminal import feature_flags as gtff -from gamestonk_terminal.helper_funcs import plot_autoscale +from gamestonk_terminal.helper_funcs import plot_autoscale, parse_known_args_and_warn + +title_maps = {} def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFrame: @@ -29,7 +32,9 @@ def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFra stock_closes: DataFrame DataFrame containing daily (adjusted) close prices for each stock in list """ - stock_prices = yf.download(list_of_stocks, period=period, group_by="ticker") + stock_prices = yf.download( + list_of_stocks, period=period, progress=False, group_by="ticker" + ) stock_closes = pd.DataFrame(index=stock_prices.index) for stock in list_of_stocks: stock_closes[stock] = stock_prices[stock]["Adj Close"] @@ -66,16 +71,19 @@ def display_weights(weights: dict): if not weights: return weight_df = pd.DataFrame.from_dict(data=weights, orient="index", columns=["value"]) - print(weight_df) + weight_df["weight"] = (weight_df["value"] * 100).astype(str) + " %" + print(weight_df["weight"]) -def pie_chart_weights(weights: dict): +def pie_chart_weights(weights: dict, optimizer: str): """ Show a pie chart of holdings Parameters ---------- weights: dict weights to display. Keys are stocks. Values are either weights or values if -v specified + optimzer: str + Optmization technique used """ if not weights: @@ -86,11 +94,15 @@ def pie_chart_weights(weights: dict): _, ax = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI) if math.isclose(sum(sizes), 1, rel_tol=0.1): wedges, _, autotexts = ax.pie( - sizes, labels=stocks, autopct="%1.1f%%", textprops=dict(color="w") + sizes, + labels=stocks, + autopct="%1.1f%%", + textprops=dict(color="k"), + labeldistance=5, ) else: wedges, _, autotexts = ax.pie( - sizes, labels=stocks, autopct="", textprops=dict(color="w") + sizes, labels=stocks, autopct="", textprops=dict(color="k"), labeldistance=5 ) for i, a in enumerate(autotexts): a.set_text(f"{sizes[i]}") @@ -110,3 +122,52 @@ def pie_chart_weights(weights: dict): plt.ion() plt.show() print("") + + +def parse_from_port_type( + parser_in: argparse.ArgumentParser, port_type: str, other_args: List[str] +): + """ + + Parameters + ---------- + parser_in: ArgumentParser + parser to get data from + port_type: str + Type of optimization that will be done. One of ["max_sharpe","min_vol", "eff_risk", "eff_ret"] + other_args: List[str] + Arguments passed to function + Returns + ------- + + ns_parser: + Parsed arguments + """ + + if port_type in ["max_sharpe", "min_volatility"]: + ns_parser = parse_known_args_and_warn(parser_in, other_args) + if not ns_parser: + return None + return ns_parser + + elif port_type == "eff_risk": + parser_in.add_argument( + "-r", "--risk", type=float, dest="risk_level", default=0.1 + ) + ns_parser = parse_known_args_and_warn(parser_in, other_args) + if not ns_parser: + return None + return ns_parser + + elif port_type == "eff_ret": + parser_in.add_argument( + "-r", "--return", type=float, dest="target_return", default=0.1 + ) + + ns_parser = parse_known_args_and_warn(parser_in, other_args) + if not ns_parser: + return None + return ns_parser + else: + print("Incorrect portfolio optimizer type\n") + return None diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_api.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py similarity index 65% rename from gamestonk_terminal/portfolio_optimization/port_opt_api.py rename to gamestonk_terminal/portfolio_optimization/optimizer_view.py index c095ef3115a..bad19006ac8 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_api.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -11,10 +11,12 @@ from pypfopt import risk_models from pypfopt import expected_returns from pypfopt import EfficientFrontier from gamestonk_terminal.helper_funcs import parse_known_args_and_warn, plot_autoscale -from gamestonk_terminal.portfolio_optimization.port_opt_helper import ( +from gamestonk_terminal.portfolio_optimization.optimizer_helper import ( process_stocks, prepare_efficient_frontier, pie_chart_weights, + display_weights, + parse_from_port_type, ) from gamestonk_terminal.config_plot import PLOT_DPI from gamestonk_terminal import feature_flags as gtff @@ -34,13 +36,13 @@ period_choices = [ ] -def equal_weight(list_of_stocks: List[str], other_args: List[str]): +def equal_weight(stocks: List[str], other_args: List[str]): """ Equally weighted portfolio, where weight = 1/# of stocks Parameters ---------- - list_of_stocks: List[str] + stocks: List[str] List of tickers to be included in optimization Returns @@ -74,29 +76,28 @@ def equal_weight(list_of_stocks: List[str], other_args: List[str]): ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return - weights = {} values = {} - n_stocks = len(list_of_stocks) - for stock in list_of_stocks: - weights[stock] = round(1 / n_stocks, 5) + n_stocks = len(stocks) + for stock in stocks: values[stock] = ns_parser.value * round(1 / n_stocks, 5) if ns_parser.pie: pie_chart_weights(values) - return values + if n_stocks >= 1: + print("Equal Weight Portfolio: ") + display_weights(values) + print("") except Exception as e: print(e) print("") -def property_weighting( - list_of_stocks: List[str], property_type: str, other_args: List[str] -): +def property_weighting(stocks: List[str], property_type: str, other_args: List[str]): """ Property weighted portfolio where each weight is the relative fraction. Parameters ---------- - list_of_stocks: List[str] + stocks: List[str] List of tickers to be included in optimization property_type: str Property to weight by. Can be anything in yfinance.Ticker().info. Examples: @@ -136,7 +137,7 @@ def property_weighting( ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return - for stock in list_of_stocks: + for stock in stocks: stock_prop = yf.Ticker(stock).info[property_type] if stock_prop is None: stock_prop = 0 @@ -144,9 +145,19 @@ def property_weighting( prop_sum += stock_prop for k, v in prop.items(): weights[k] = round(v / prop_sum, 5) * ns_parser.value + if ns_parser.pie: pie_chart_weights(weights) - return weights + + if property_type == "marketCap": + print("Market Cap Weighted Portfolio: ") + + elif property_type == "dividendYield": + print("Dividend Yield Weighted Portfolio: ") + + if len(stocks) >= 1: + display_weights(weights) + print("") except Exception as e: print(e) @@ -154,7 +165,114 @@ def property_weighting( return -def show_ef(list_of_stocks: List[str], other_args: List[str]): +def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): + """ + Return a portfolio based on condition in port_type Defaults to 3m of historical data + Parameters + ---------- + stocks: List[str] + List of the stocks to be included in the weights + port_type: str + Method to be used on ef object (example: max_sharpe, min_volatility) + Returns + ------- + weights: dict + Dictionary of weights where keys are the tickers. + """ + + parser = argparse.ArgumentParser(add_help=False, prog="port_type") + + parser.add_argument( + "-p", + "--period", + default="3mo", + dest="period", + help="period to get yfinance data from", + choices=period_choices, + ) + + parser.add_argument( + "-v", + "--value", + dest="value", + help="Amount to allocate to portfolio", + type=float, + default=1.0, + ) + parser.add_argument( + "--pie", + action="store_true", + dest="pie", + default=False, + help="Display a pie chart for weights", + ) + + try: + ns_parser = parse_from_port_type(parser, port_type, other_args) + if not ns_parser: + return + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return + period = ns_parser.period + stock_prices = process_stocks(stocks, period) + ef = prepare_efficient_frontier(stock_prices) + + if port_type == "max_sharpe": + + ef_sharpe = dict(ef.max_sharpe()) + weights = { + key: ns_parser.value * round(value, 5) + for key, value in ef_sharpe.items() + } + print("Weights that maximize Sharpe Ratio:") + + elif port_type == "min_volatility": + + ef_min_vol = dict(ef.min_volatility()) + weights = { + key: ns_parser.value * round(value, 5) + for key, value in ef_min_vol.items() + } + print("Weights that minimize volatility") + + elif port_type == "eff_risk": + + ef_eff_risk = dict(ef.efficient_risk(ns_parser.risk_level)) + weights = { + key: ns_parser.value * round(value, 5) + for key, value in ef_eff_risk.items() + } + print("Weights for maximizing returns at your risk level:") + + elif port_type == "eff_ret": + + ef_eff_risk = dict(ef.efficient_return(ns_parser.target_return)) + weights = { + key: ns_parser.value * round(value, 5) + for key, value in ef_eff_risk.items() + } + print("Weights for minimizing risk at your return:") + + else: + raise ValueError("EF Method not found") + + if ns_parser.pie: + pie_chart_weights(weights, port_type) + + print("") + ef.portfolio_performance(verbose=True) + print("") + display_weights(weights) + print("") + + except Exception as e: + print(e) + print("") + return + + +def show_ef(stocks: List[str], other_args: List[str]): parser = argparse.ArgumentParser(add_help=False, prog="ef") parser.add_argument( @@ -172,8 +290,12 @@ def show_ef(list_of_stocks: List[str], other_args: List[str]): try: ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: - return {} - stock_prices = process_stocks(list_of_stocks, ns_parser.period) + return + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return + + stock_prices = process_stocks(stocks, ns_parser.period) mu = expected_returns.mean_historical_return(stock_prices) S = risk_models.sample_cov(stock_prices) ef = EfficientFrontier(mu, S) @@ -209,152 +331,3 @@ def show_ef(list_of_stocks: List[str], other_args: List[str]): except Exception as e: print(e) print("") - - -def ef_portfolio(list_of_stocks: List[str], port_type: str, other_args: List[str]): - """ - Return a portfolio based on condition in port_type Defaults to 3m of historical data - Parameters - ---------- - list_of_stocks: List[str] - List of the stocks to be included in the weights - port_type: str - Method to be used on ef object (example: max_sharpe, min_volatility) - Returns - ------- - weights: dict - Dictionary of weights where keys are the tickers. - """ - - parser = argparse.ArgumentParser(add_help=False, prog="ef") - - parser.add_argument( - "-p", - "--period", - default="3mo", - dest="period", - help="period to get yfinance data from", - choices=period_choices, - ) - - parser.add_argument( - "-v", - "--value", - dest="value", - help="Amount to allocate to portfolio", - type=float, - default=1.0, - ) - parser.add_argument( - "--pie", - action="store_true", - dest="pie", - default=False, - help="Display a pie chart for weights", - ) - if port_type == "max_sharpe": - try: - ns_parser = parse_known_args_and_warn(parser, other_args) - if not ns_parser: - return {} - period = ns_parser.period - stock_prices = process_stocks(list_of_stocks, period) - ef = prepare_efficient_frontier(stock_prices) - ef_sharpe = dict(ef.max_sharpe()) - weights = { - key: ns_parser.value * round(value, 5) - for key, value in ef_sharpe.items() - } - if ns_parser.pie: - pie_chart_weights(weights) - print("") - ef.portfolio_performance(verbose=True) - print("") - return weights - except Exception as e: - print(e) - print("") - return {} - - elif port_type == "min_volatility": - try: - ns_parser = parse_known_args_and_warn(parser, other_args) - if not ns_parser: - return {} - period = ns_parser.period - stock_prices = process_stocks(list_of_stocks, period) - ef = prepare_efficient_frontier(stock_prices) - ef_min_vol = dict(ef.min_volatility()) - weights = { - key: ns_parser.value * round(value, 5) - for key, value in ef_min_vol.items() - } - if ns_parser.pie: - pie_chart_weights(weights) - print("") - ef.portfolio_performance(verbose=True) - print("") - return weights - - except Exception as e: - print(e) - print("") - return {} - - elif port_type == "eff_risk": - - parser.add_argument("-r", "--risk", type=float, dest="risk_level", default=0.1) - try: - ns_parser = parse_known_args_and_warn(parser, other_args) - if not ns_parser: - return - if len(list_of_stocks) == 0: - return {} - stock_prices = process_stocks(list_of_stocks, ns_parser.period) - ef = prepare_efficient_frontier(stock_prices) - ef_eff_risk = dict(ef.efficient_risk(ns_parser.risk_level)) - weights = { - key: ns_parser.value * round(value, 5) - for key, value in ef_eff_risk.items() - } - if ns_parser.pie: - pie_chart_weights(weights) - print("") - ef.portfolio_performance(verbose=True) - print("") - return weights - except Exception as e: - print(e) - print("") - return {} - - elif port_type == "eff_ret": - - parser.add_argument( - "-r", "--return", type=float, dest="target_return", default=0.1 - ) - try: - ns_parser = parse_known_args_and_warn(parser, other_args) - if not ns_parser: - return - stock_prices = process_stocks(list_of_stocks, ns_parser.period) - ef = prepare_efficient_frontier(stock_prices) - ef_eff_risk = dict(ef.efficient_return(ns_parser.target_return)) - weights = { - key: ns_parser.value * round(value, 5) - for key, value in ef_eff_risk.items() - } - if ns_parser.pie: - pie_chart_weights(weights) - print("") - ef.portfolio_performance(verbose=True) - print("") - return weights - - except Exception as e: - print(e) - print("") - return {} - - else: - raise ValueError("EF Method not found") diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index cda42f940cf..fa515d0811b 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -9,8 +9,8 @@ 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 port_opt_api as po_api -from gamestonk_terminal.portfolio_optimization.port_opt_helper import display_weights +from gamestonk_terminal.portfolio_optimization import optimizer_view as po_api +from gamestonk_terminal.portfolio_optimization.optimizer_helper import display_weights class PortfolioOptimization: @@ -105,46 +105,25 @@ class PortfolioOptimization: self.add_stocks(self, other_args) def call_equal_weight(self, other_args: List[str]): - weights = po_api.equal_weight(self.tickers, other_args) - print("Optimal Weights for Equal Weighting:") - display_weights(weights) - print("") + po_api.equal_weight(self.tickers, other_args) def call_mkt_cap(self, other_args: List[str]): - weights = po_api.property_weighting(self.tickers, "marketCap", other_args) - print("Market Cap Weighting Weights:") - display_weights(weights) - print("") + po_api.property_weighting(self.tickers, "marketCap", other_args) def call_div_yield(self, other_args: List[str]): - weights = po_api.property_weighting(self.tickers, "dividendYield", other_args) - print("Dividend Weighed Weights:") - display_weights(weights) - print("") + po_api.property_weighting(self.tickers, "dividendYield", other_args) def call_max_sharpe(self, other_args: List[str]): - weights = po_api.ef_portfolio(self.tickers, "max_sharpe", other_args) - print("Maximum Sharpe Weights:") - display_weights(weights) - print("") + po_api.ef_portfolio(self.tickers, "max_sharpe", other_args) def call_min_vol(self, other_args: List[str]): - weights = po_api.ef_portfolio(self.tickers, "min_volatility", other_args) - print("Minimum volatility Weights:") - display_weights(weights) - print("") + po_api.ef_portfolio(self.tickers, "min_volatility", other_args) def call_eff_risk(self, other_args: List[str]): - weights = po_api.ef_portfolio(self.tickers, "eff_risk", other_args) - print("Weights for max returns at risk level") - display_weights(weights) - print("") + po_api.ef_portfolio(self.tickers, "eff_risk", other_args) def call_eff_ret(self, other_args: List[str]): - weights = po_api.ef_portfolio(self.tickers, "eff_ret", other_args) - print("Weights for min risk at target returns") - display_weights(weights) - print("") + po_api.ef_portfolio(self.tickers, "eff_ret", other_args) def call_show_ef(self, other_args): po_api.show_ef(self.tickers, other_args)