From 57d56b8b1e2903fada239270b2c3cb59f5ea3e5d Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 00:38:14 +0100 Subject: [PATCH] some more minor refactoring cuz im picky --- gamestonk_terminal/helper_funcs.py | 2 +- .../optimizer_helper.py | 176 +++++++----- .../portfolio_optimization/optimizer_view.py | 256 +++++++++++++----- .../portfolio_optimization/po_controller.py | 55 ++-- 4 files changed, 335 insertions(+), 154 deletions(-) diff --git a/gamestonk_terminal/helper_funcs.py b/gamestonk_terminal/helper_funcs.py index 11a3ffbe196..a2e00fd3d35 100644 --- a/gamestonk_terminal/helper_funcs.py +++ b/gamestonk_terminal/helper_funcs.py @@ -347,7 +347,7 @@ def patch_pandas_text_adjustment(): def parse_known_args_and_warn(parser, l_args): parser.add_argument( - "-h", "--help", action="store_true", dest="help", help="show this help message" + "-h", "--help", action="store_true", help="show this help message" ) (ns_parser, l_unknown_args) = parser.parse_known_args(l_args) diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py index 24db45eed91..19f9057bd76 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py @@ -15,15 +15,107 @@ 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 = { - "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", -} +l_valid_property_infos = [ + "previousClose", + "regularMarketOpen", + "twoHundredDayAverage", + "trailingAnnualDividendYield", + "payoutRatio", + "volume24Hr", + "regularMarketDayHigh", + "navPrice", + "averageDailyVolume10Day", + "totalAssets", + "regularMarketPreviousClose", + "fiftyDayAverage", + "trailingAnnualDividendRate", + "open", + "toCurrency", + "averageVolume10days", + "expireDate", + "yield", + "algorithm", + "dividendRate", + "exDividendDate", + "beta", + "circulatingSupply", + "regularMarketDayLow", + "priceHint", + "currency", + "trailingPE", + "regularMarketVolume", + "lastMarket", + "maxSupply", + "openInterest", + "marketCap", + "volumeAllCurrencies", + "strikePrice", + "averageVolume", + "priceToSalesTrailing12Months", + "dayLow", + "ask", + "ytdReturn", + "askSize", + "volume", + "fiftyTwoWeekHigh", + "forwardPE", + "fromCurrency", + "fiveYearAvgDividendYield", + "fiftyTwoWeekLow", + "bid", + "dividendYield", + "bidSize", + "dayHigh", + "annualHoldingsTurnover", + "enterpriseToRevenue", + "beta3Year", + "profitMargins", + "enterpriseToEbitda", + "52WeekChange", + "morningStarRiskRating", + "forwardEps", + "revenueQuarterlyGrowth", + "sharesOutstanding", + "fundInceptionDate", + "annualReportExpenseRatio", + "bookValue", + "sharesShort", + "sharesPercentSharesOut", + "fundFamily", + "lastFiscalYearEnd", + "heldPercentInstitutions", + "netIncomeToCommon", + "trailingEps", + "lastDividendValue", + "SandP52WeekChange", + "priceToBook", + "heldPercentInsiders", + "shortRatio", + "sharesShortPreviousMonthDate", + "floatShares", + "enterpriseValue", + "threeYearAverageReturn", + "lastSplitFactor", + "legalType", + "lastDividendDate", + "morningStarOverallRating", + "earningsQuarterlyGrowth", + "pegRatio", + "lastCapGain", + "shortPercentOfFloat", + "sharesShortPriorMonth", + "impliedSharesOutstanding", + "fiveYearAverageReturn", + "regularMarketPrice", +] + + +def check_valid_property_type(property: str) -> str: + """ Check that the property selected is valid """ + if property in l_valid_property_infos: + return property + else: + raise argparse.ArgumentTypeError(f"{property} is not a valid info") def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFrame: @@ -97,15 +189,15 @@ def my_autopct(x): return "" -def pie_chart_weights(weights: dict, optimizer: str, value: float): - """ - Show a pie chart of holdings +def pie_chart_weights(weights: dict, title_opt: 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 - Optimization technique used for title + 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 """ if not weights: return @@ -176,10 +268,7 @@ def pie_chart_weights(weights: dict, optimizer: str, value: float): plt.setp(autotexts, size=8, weight="bold") - if optimizer in ["eff_ret", "eff_risk"]: - plt.gca().set_title(title_maps[optimizer].format(100 * value), pad=20) - else: - plt.gca().set_title(title_maps[optimizer], pad=20) + plt.gca().set_title(title_opt, pad=20) if gtff.USE_ION: plt.ion() @@ -188,52 +277,3 @@ def pie_chart_weights(weights: dict, optimizer: str, value: float): 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/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py index 6e779b0669a..4f7170e1fd8 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -16,7 +16,7 @@ from gamestonk_terminal.portfolio_optimization.optimizer_helper import ( prepare_efficient_frontier, pie_chart_weights, display_weights, - parse_from_port_type, + check_valid_property_type, ) from gamestonk_terminal.config_plot import PLOT_DPI from gamestonk_terminal import feature_flags as gtff @@ -35,6 +35,20 @@ period_choices = [ "max", ] +d_period = { + "1d": "[1 Day]", + "5d": "[5 Days]", + "1mo": "[1 Month]", + "3mo": "[3 Months]", + "6mo": "[6 Months]", + "1y": "[1 Year]", + "2y": "[2 Years]", + "5y": "[5 Years]", + "10y": "[10 Years]", + "ytd": "[Year-to-Date]", + "max": "[All-time]", +} + def equal_weight(stocks: List[str], other_args: List[str]): """ @@ -53,8 +67,8 @@ def equal_weight(stocks: List[str], other_args: List[str]): """ parser = argparse.ArgumentParser( add_help=False, - prog="equal_weight", - description="Return equally weighted portfolio holdings", + prog="equal", + description="Returns an equally weighted portfolio", ) parser.add_argument( @@ -83,7 +97,7 @@ def equal_weight(stocks: List[str], other_args: List[str]): for stock in stocks: values[stock] = ns_parser.value * round(1 / len(stocks), 5) if ns_parser.pie: - pie_chart_weights(values, "equal", 0) + pie_chart_weights(values, "Equally Weighted Portfolio") else: display_weights(values) print("") @@ -93,16 +107,15 @@ def equal_weight(stocks: List[str], other_args: List[str]): print("") -def property_weighting(stocks: List[str], property_type: str, other_args: List[str]): - """ - Property weighted portfolio where each weight is the relative fraction. +def property_weighting(stocks: List[str], other_args: List[str]): + """Weighted portfolio where each weight is the relative fraction of a specified info property. + Parameters ---------- 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: - "marketCap", "dividendYield", etc + other_args : List[str] + Command line arguments to be processed with argparse Returns ------- @@ -111,10 +124,32 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s """ parser = argparse.ArgumentParser( add_help=False, - prog="market_cap_weighted", - description="Return portfolio weights/values that are weighted by marketcap", + prog="property", + description="Returns a portfolio that is weighted based on a selected property info", + ) + parser.add_argument( + "-p", + "--property", + required=True, + type=check_valid_property_type, + dest="property", + help="""Property info to weigh. Use one of: + previousClose, regularMarketOpen, twoHundredDayAverage, trailingAnnualDividendYield, + payoutRatio, volume24Hr, regularMarketDayHigh, navPrice, averageDailyVolume10Day, totalAssets, + regularMarketPreviousClose, fiftyDayAverage, trailingAnnualDividendRate, open, toCurrency, averageVolume10days, + expireDate, yield, algorithm, dividendRate, exDividendDate, beta, circulatingSupply, regularMarketDayLow, + priceHint, currency, trailingPE, regularMarketVolume, lastMarket, maxSupply, openInterest, marketCap, + volumeAllCurrencies, strikePrice, averageVolume, priceToSalesTrailing12Months, dayLow, ask, ytdReturn, askSize, + volume, fiftyTwoWeekHigh, forwardPE, fromCurrency, fiveYearAvgDividendYield, fiftyTwoWeekLow, bid, dividendYield, + bidSize, dayHigh, annualHoldingsTurnover, enterpriseToRevenue, beta3Year, profitMargins, enterpriseToEbitda, + 52WeekChange, morningStarRiskRating, forwardEps, revenueQuarterlyGrowth, sharesOutstanding, fundInceptionDate, + annualReportExpenseRatio, bookValue, sharesShort, sharesPercentSharesOut, fundFamily, lastFiscalYearEnd, + heldPercentInstitutions, netIncomeToCommon, trailingEps, lastDividendValue, SandP52WeekChange, priceToBook, + heldPercentInsiders, shortRatio, sharesShortPreviousMonthDate, floatShares, enterpriseValue, + threeYearAverageReturn, lastSplitFactor, legalType, lastDividendDate, morningStarOverallRating, + earningsQuarterlyGrowth, pegRatio, lastCapGain, shortPercentOfFloat, sharesShortPriorMonth, + impliedSharesOutstanding, fiveYearAverageReturn, and regularMarketPrice.""", ) - parser.add_argument( "-v", "--value", @@ -130,19 +165,24 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s default=False, help="Display a pie chart for weights", ) - weights = {} - prop = {} - prop_sum = 0 try: + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-p") + 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 + + weights = {} + prop = {} + prop_sum = 0 for stock in stocks: - stock_prop = yf.Ticker(stock).info[property_type] + stock_prop = yf.Ticker(stock).info[ns_parser.property] if stock_prop is None: stock_prop = 0 prop[stock] = stock_prop @@ -151,7 +191,9 @@ 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, property_type, 0) + pie_chart_weights( + weights, "Weighted Portfolio based on " + ns_parser.property + ) else: display_weights(weights) print("") @@ -165,20 +207,37 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s 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) + Method to be used on ef object (example: max_sharpe, min_volatility, eff_risk, eff_ret) + Returns ------- weights: dict Dictionary of weights where keys are the tickers. """ - parser = argparse.ArgumentParser(add_help=False, prog=port_type) + d_descriptions = { + "max_sharpe": """Maximise the Sharpe Ratio. The result is also referred to as the tangency portfolio, + as it is the portfolio for which the capital market line is tangent to the efficient frontier. + This is a convex optimization problem after making a certain variable substitution. See + Cornuejols and Tutuncu (2006) for more.""", + "min_volatility": "optimizes for minimum volatility", + "max_quadratic_utility": "maximises the quadratic utility, given some risk aversion", + "efficient_risk": """Maximise return for a target risk. The resulting portfolio will have + a volatility less than the target (but not guaranteed to be equal).""", + "efficient_return": "Calculate the 'Markowitz portfolio', minimising volatility for a given target return.", + "max_quadratic_utility": "Maximise the given quadratic utility, i.e: max_w w^T \mu - \frac \delta 2 w^T \Sigma w", + } + + parser = argparse.ArgumentParser( + add_help=False, prog=port_type, description=d_descriptions[port_type] + ) parser.add_argument( "-p", "--period", @@ -187,7 +246,6 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): help="period to get yfinance data from", choices=period_choices, ) - parser.add_argument( "-v", "--value", @@ -204,68 +262,144 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): help="Display a pie chart for weights", ) + if port_type == "max_sharpe": + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-r") + parser.add_argument( + "-r", + "--risk_free_rate", + type=float, + dest="risk_free_rate", + default=0.02, + help="""Risk-free rate of borrowing/lending. The period of the risk-free rate + should correspond to the frequency of expected returns.""", + ) + + elif port_type == "efficient_risk": + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-t") + parser.add_argument( + "-t", + "--target_volatility", + type=float, + dest="target_volatility", + default=0.1, + help="The desired maximum volatility of the resulting portfolio", + ) + parser.add_argument( + "-n", + "--market_neutral", + action="store_true", + default=False, + dest="market_neutral", + help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False. + Requires negative lower weight bound.""", + ) + + elif port_type == "efficient_return": + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-t") + parser.add_argument( + "-t", + "--target_return", + type=float, + dest="target_return", + default=0.1, + help="the desired return of the resulting portfolio", + ) + parser.add_argument( + "-n", + "--market_neutral", + action="store_true", + default=False, + dest="market_neutral", + help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False. + Requires negative lower weight bound.""", + ) + + elif port_type == "max_quadratic_utility": + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-r") + parser.add_argument( + "-r", + "--risk_aversion", + type=float, + dest="risk_aversion", + default=1, + help="risk aversion parameter", + ) + parser.add_argument( + "-n", + "--market_neutral", + action="store_true", + default=False, + dest="market_neutral", + help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False. + Requires negative lower weight bound.""", + ) + try: - ns_parser = parse_from_port_type(parser, port_type, other_args) + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: - return + return None 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": + sp = d_period[ns_parser.period] - ef_sharpe = dict(ef.max_sharpe()) - weights = { - key: ns_parser.value * round(value, 5) - for key, value in ef_sharpe.items() - } - val = 0 - print("Weights that maximize Sharpe Ratio:") + if port_type == "max_sharpe": + ef_opt = dict(ef.max_sharpe(ns_parser.risk_free_rate)) + s_title = f"{sp} Weights that maximize Sharpe ration with risk free level of {ns_parser.risk_free_rate}" elif port_type == "min_volatility": + ef_opt = dict(ef.min_volatility()) + s_title = f"{sp} Weights that minimize volatility" - ef_min_vol = dict(ef.min_volatility()) - weights = { - key: ns_parser.value * round(value, 5) - for key, value in ef_min_vol.items() - } - val = 0 - print("Weights that minimize volatility") + elif port_type == "efficient_risk": + ef_opt = dict( + ef.efficient_risk(ns_parser.target_volatility, ns_parser.market_neutral) + ) + s_title = f"{sp} Weights that maximize return with a maximum volatility of {ns_parser.target_volatility}" - elif port_type == "eff_risk": + elif port_type == "efficient_return": + ef_opt = dict( + ef.efficient_return(ns_parser.target_return, ns_parser.market_neutral) + ) + s_title = f"{sp} Weights that minimise volatility for a given target return of {ns_parser.target_return}" - 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() - } - val = ns_parser.risk_level - print(f"Weights for maximizing returns at risk = {100*val:.1f} %") - - 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() - } - val = ns_parser.target_return - print(f"Weights for minimizing risk at target return = {100*val:.1f} %") + elif port_type == "max_quadratic_utility": + ef_opt = dict( + ef.max_quadratic_utility( + ns_parser.risk_aversion, ns_parser.market_neutral + ) + ) + s_title = f"{sp} Weights that maximise the quadratic utility with risk aversion of {ns_parser.risk_aversion}" else: raise ValueError("EF Method not found") - if ns_parser.pie: - pie_chart_weights(weights, port_type, val) + weights = { + key: ns_parser.value * round(value, 5) for key, value in ef_opt.items() + } + + if ns_parser.pie: + pie_chart_weights(weights, s_title) + else: + print(s_title) + display_weights(weights) + print("") - print("") ef.portfolio_performance(verbose=True) print("") - display_weights(weights) - print("") except Exception as e: print(e) diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index d4d2960adee..24a93bcdd5e 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -21,12 +21,12 @@ class PortfolioOptimization: "select", "add", "equal", - "mktcap", - "dividend", + "property", "maxsharpe", "minvol", - "maxret", - "minrisk", + "effret", + "effrisk", + "maxquadutil", "ef", "yolo", ] @@ -57,12 +57,20 @@ class PortfolioOptimization: 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( + " property weight according to selected info property (e.g. marketCap)" + ) + print("") + print( + " maxsharpe optimizes for maximal Sharpe ratio (a.k.a the tangency portfolio)" + ) + print(" minvol optimizes for minimum volatility") + print( + " maxquadutil maximises the quadratic utility, given some risk aversion" + ) + print(" effret maximises return for a given target risk") + print(" effrisk minimises risk for a given target return") + print("") print(" ef show the efficient frontier") print("") @@ -107,13 +115,9 @@ class PortfolioOptimization: """Process equal command""" optimizer_view.equal_weight(self.tickers, other_args) - def call_mktcap(self, other_args: List[str]): - """Process mktcap command""" - optimizer_view.property_weighting(self.tickers, "marketCap", other_args) - - def call_dividend(self, other_args: List[str]): - """Process dividend command""" - optimizer_view.property_weighting(self.tickers, "dividendYield", other_args) + def call_property(self, other_args: List[str]): + """Process property command""" + optimizer_view.property_weighting(self.tickers, other_args) def call_maxsharpe(self, other_args: List[str]): """Process maxsharpe command""" @@ -123,13 +127,17 @@ class PortfolioOptimization: """Process minvol command""" optimizer_view.ef_portfolio(self.tickers, "min_volatility", 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_maxquadutil(self, other_args: List[str]): + """Process maxquadutil command""" + optimizer_view.ef_portfolio(self.tickers, "max_quadratic_utility", 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_effrisk(self, other_args: List[str]): + """Process effrisk command""" + optimizer_view.ef_portfolio(self.tickers, "efficient_risk", other_args) + + def call_effret(self, other_args: List[str]): + """Process effret command""" + optimizer_view.ef_portfolio(self.tickers, "efficient_return", other_args) def call_ef(self, other_args): """Process ef command""" @@ -158,7 +166,6 @@ class PortfolioOptimization: help="add tickers to optimzation.", ) try: - if other_args: if "-" not in other_args[0]: other_args.insert(0, "-t")