diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py index af081ca4708..f54ccdc3ea3 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -94,9 +94,11 @@ 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 = {} for stock in stocks: values[stock] = ns_parser.value * round(1 / len(stocks), 5) @@ -208,38 +210,22 @@ def property_weighting(stocks: List[str], other_args: List[str]): return -def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): +def max_sharpe(stocks: List[str], other_args: List[str]): """ - Return a portfolio based on condition in port_type Defaults to 3m of historical data + Return a portfolio that maximises the Sharpe Ratio 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, eff_risk, eff_ret) Returns ------- weights: dict Dictionary of weights where keys are the tickers. """ - - 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.", - } - parser = argparse.ArgumentParser( - add_help=False, prog=port_type, description=d_descriptions[port_type] + add_help=False, prog="maxsharpe", description="Maximise the Sharpe Ratio" ) parser.add_argument( "-p", @@ -265,90 +251,24 @@ 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.""", - ) + 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.""", + ) try: ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return None + if len(stocks) < 2: print("Please have at least 2 loaded tickers to calculate weights.\n") return @@ -359,36 +279,8 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): sp = d_period[ns_parser.period] - if port_type == "max_sharpe": - ef_opt = dict(ef.max_sharpe(ns_parser.risk_free_rate)) - s_title = f"{sp} Weights that maximize Sharpe ratio 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" - - 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 == "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}" - - 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") + ef_opt = dict(ef.max_sharpe(ns_parser.risk_free_rate)) + s_title = f"{sp} Weights that maximize Sharpe ratio with risk free level of {ns_parser.risk_free_rate}" weights = { key: ns_parser.value * round(value, 5) for key, value in ef_opt.items() @@ -410,6 +302,414 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): return +def min_volatility(stocks: List[str], other_args: List[str]): + """ + Return a portfolio that optimizes for minimum volatility + + Parameters + ---------- + stocks: List[str] + List of the stocks to be included in the weights + + Returns + ------- + weights: dict + Dictionary of weights where keys are the tickers. + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="min_volatility", + description="Optimizes for minimum volatility", + ) + 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_known_args_and_warn(parser, other_args) + if not ns_parser: + 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) + + sp = d_period[ns_parser.period] + + ef_opt = dict(ef.min_volatility()) + s_title = f"{sp} Weights that minimize volatility" + + 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("") + + ef.portfolio_performance(verbose=True) + print("") + + except Exception as e: + print(e) + print("") + return + + +def max_quadratic_utility(stocks: List[str], other_args: List[str]): + """ + Return a portfolio that maximises the quadratic utility, given some risk aversion + + Parameters + ---------- + stocks: List[str] + List of the stocks to be included in the weights + + Returns + ------- + weights: dict + Dictionary of weights where keys are the tickers. + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="max_quadratic_utility", + description="Maximises the quadratic utility, given some risk aversion", + ) + 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( + "-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.""", + ) + if "-n" not in other_args: + parser.add_argument( + "--pie", + action="store_true", + dest="pie", + default=False, + help="Display a pie chart for weights. Only if neutral flag is left False.", + ) + + 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", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + 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) + + sp = d_period[ns_parser.period] + + 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}" + + weights = { + key: ns_parser.value * round(value, 5) for key, value in ef_opt.items() + } + + if not ns_parser.market_neutral: + if ns_parser.pie: + pie_chart_weights(weights, s_title) + ef.portfolio_performance(verbose=True) + print("") + return + + print(s_title) + display_weights(weights) + print("") + ef.portfolio_performance(verbose=True) + print("") + + except Exception as e: + print(e) + print("") + return + + +def efficient_risk(stocks: List[str], other_args: List[str]): + """ + Return a portfolio that maximises return for a target risk. The resulting portfolio will have + a volatility less than the target (but not guaranteed to be equal) + + Parameters + ---------- + stocks: List[str] + List of the stocks to be included in the weights + + Returns + ------- + weights: dict + Dictionary of weights where keys are the tickers. + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="effrisk", + description="""Maximise return for a target risk. The resulting portfolio will have + a volatility less than the target (but not guaranteed to be equal)""", + ) + 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( + "-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.""", + ) + if "-n" not in other_args: + parser.add_argument( + "--pie", + action="store_true", + dest="pie", + default=False, + help="Display a pie chart for weights. Only if neutral flag is left False.", + ) + + 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", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + 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) + + sp = d_period[ns_parser.period] + + 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}" + + weights = { + key: ns_parser.value * round(value, 5) for key, value in ef_opt.items() + } + + if not ns_parser.market_neutral: + if ns_parser.pie: + pie_chart_weights(weights, s_title) + ef.portfolio_performance(verbose=True) + print("") + return + + print(s_title) + display_weights(weights) + print("") + ef.portfolio_performance(verbose=True) + print("") + + except Exception as e: + print(e) + print("") + return + + +def efficient_return(stocks: List[str], other_args: List[str]): + """ + Return a portfolio that minimises volatility for a given target return ('Markowitz portfolio') + + Parameters + ---------- + stocks: List[str] + List of the stocks to be included in the weights + + Returns + ------- + weights: dict + Dictionary of weights where keys are the tickers. + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="effret", + description="Calculate the 'Markowitz portfolio', minimising volatility for a given target return", + ) + 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( + "-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.""", + ) + if "-n" not in other_args: + parser.add_argument( + "--pie", + action="store_true", + dest="pie", + default=False, + help="Display a pie chart for weights. Only if neutral flag is left False.", + ) + + 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", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + 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) + + sp = d_period[ns_parser.period] + + 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}" + + weights = { + key: ns_parser.value * round(value, 5) for key, value in ef_opt.items() + } + + if not ns_parser.market_neutral: + if ns_parser.pie: + pie_chart_weights(weights, s_title) + ef.portfolio_performance(verbose=True) + print("") + return + + print(s_title) + display_weights(weights) + print("") + ef.portfolio_performance(verbose=True) + 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, diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index a7ef204f128..398cbc883ab 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -5,11 +5,13 @@ __docformat__ = "numpy" import argparse from typing import List import matplotlib.pyplot as plt +from datetime import datetime 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 +from gamestonk_terminal.comparison_analysis import ca_controller class PortfolioOptimization: @@ -28,6 +30,7 @@ class PortfolioOptimization: "effrisk", "maxquadutil", "ef", + "ca", "yolo", ] @@ -74,6 +77,9 @@ class PortfolioOptimization: print("") print(" ef show the efficient frontier") print("") + if tickers: + print(" > ca comparison analysis for selected tickers") + print("") def switch(self, an_input: str): """Process and dispatch input @@ -122,23 +128,23 @@ class PortfolioOptimization: def call_maxsharpe(self, other_args: List[str]): """Process maxsharpe command""" - optimizer_view.ef_portfolio(self.tickers, "max_sharpe", other_args) + optimizer_view.max_sharpe(self.tickers, other_args) def call_minvol(self, other_args: List[str]): """Process minvol command""" - optimizer_view.ef_portfolio(self.tickers, "min_volatility", other_args) + optimizer_view.min_volatility(self.tickers, other_args) def call_maxquadutil(self, other_args: List[str]): """Process maxquadutil command""" - optimizer_view.ef_portfolio(self.tickers, "max_quadratic_utility", other_args) + optimizer_view.max_quadratic_utility(self.tickers, other_args) def call_effrisk(self, other_args: List[str]): """Process effrisk command""" - optimizer_view.ef_portfolio(self.tickers, "efficient_risk", other_args) + optimizer_view.efficient_risk(self.tickers, other_args) def call_effret(self, other_args: List[str]): """Process effret command""" - optimizer_view.ef_portfolio(self.tickers, "efficient_return", other_args) + optimizer_view.efficient_return(self.tickers, other_args) def call_ef(self, other_args): """Process ef command"""