From ccd322d6b60c9755154b3d86ed1b47f815c8dc12 Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 11 Apr 2021 16:15:11 -0400 Subject: [PATCH 01/31] add basic portfolio optimization controller --- .../comparison_analysis/ca_controller.py | 8 + gamestonk_terminal/main_helper.py | 1 + .../portfolio_optimization/README.md | 16 ++ .../portfolio_optimization/__init__.py | 0 .../portfolio_optimization/port_opt_api.py | 28 +++ .../port_optimization_controller.py | 160 ++++++++++++++++++ terminal.py | 6 + 7 files changed, 219 insertions(+) create mode 100644 gamestonk_terminal/portfolio_optimization/README.md create mode 100644 gamestonk_terminal/portfolio_optimization/__init__.py create mode 100644 gamestonk_terminal/portfolio_optimization/port_opt_api.py create mode 100644 gamestonk_terminal/portfolio_optimization/port_optimization_controller.py diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py index 67b91f8b4da..9c4aaa89258 100644 --- a/gamestonk_terminal/comparison_analysis/ca_controller.py +++ b/gamestonk_terminal/comparison_analysis/ca_controller.py @@ -15,6 +15,7 @@ from gamestonk_terminal.comparison_analysis import yahoo_finance_api as yf_api from gamestonk_terminal.comparison_analysis import market_watch_api as mw_api from gamestonk_terminal.comparison_analysis import finbrain_api as f_api from gamestonk_terminal.comparison_analysis import finviz_compare_view +from gamestonk_terminal.portfolio_optimization import port_optimization_controller from gamestonk_terminal.menu import session from prompt_toolkit.completion import NestedCompleter @@ -42,6 +43,7 @@ class ComparisonAnalysisController: "ownership", "performance", "technical", + "po" ] def __init__( @@ -107,6 +109,8 @@ class ComparisonAnalysisController: print(" performance brief performance comparison") print(" technical brief technical comparison") print("") + print(" po portfolio optimzation from selected tickers") + print("") return @staticmethod @@ -312,6 +316,10 @@ class ComparisonAnalysisController: """Process technical command""" finviz_compare_view.screener(other_args, "technical", self.ticker, self.similar) + def call_po(self, _): + """Open Portfolio Optimization menu with ticker and similar""" + port_optimization_controller.menu([self.ticker] + self.similar) + def menu(stock: pd.DataFrame, ticker: str, start: datetime, interval: str): """Comparison Analysis Menu""" diff --git a/gamestonk_terminal/main_helper.py b/gamestonk_terminal/main_helper.py index 81238c99223..8b152e09909 100644 --- a/gamestonk_terminal/main_helper.py +++ b/gamestonk_terminal/main_helper.py @@ -60,6 +60,7 @@ def print_help(s_ticker, s_start, s_interval, b_is_market_open): ) print(" pa portfolio analysis, \t\t supports: robinhood, alpaca, ally ") print(" crypto cryptocurrencies, \t\t uses coingecko api") + print(" po portfolio analysis, \t\t UPDATE ME BEFORE PR") if s_ticker: print( diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md new file mode 100644 index 00000000000..b546337b1be --- /dev/null +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -0,0 +1,16 @@ +# Portfolio Optimization + +This menu aims to discover optimal portfolios for selected stocks + + +###add +Adds tickers to current list for optimization +```` +usage: add ticker1,ticker2,... +```` +###optimize +Runs optimization +```` +usage: optimize +```` + diff --git a/gamestonk_terminal/portfolio_optimization/__init__.py b/gamestonk_terminal/portfolio_optimization/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_api.py b/gamestonk_terminal/portfolio_optimization/port_opt_api.py new file mode 100644 index 00000000000..c4654af9965 --- /dev/null +++ b/gamestonk_terminal/portfolio_optimization/port_opt_api.py @@ -0,0 +1,28 @@ +""" Portfolio Optimization Functions""" +__docformat__ = "numpy" + +import argparse +from typing import List + +def optimize(list_of_stocks:List, other_args:List[str]): + """ + + Parameters + ---------- + list_of_stocks: List[str] + List of tickers to be included in optimization + + Returns + ------- + weights : dict + Dictionary of weights where keys are the tickers + + """ + weights = {} + n_stocks = len(list_of_stocks) + for stock in list_of_stocks: + weights[stock] = round(1/n_stocks, 5) + + return weights + + diff --git a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py new file mode 100644 index 00000000000..b90bb4b8b8d --- /dev/null +++ b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py @@ -0,0 +1,160 @@ +"""Portfolio Optimization Controller Module""" +__docformat__ = "numpy" + + +import argparse +from typing import List, Set +from datetime import datetime + +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 prompt_toolkit.completion import NestedCompleter + +class PortfolioOptimization: + + CHOICES = ["help", + "q", + "quit", + "clear", + "add", + "optimize"] + + def __init__(self, + tickers:Set[str] = None + ): + """ + Construct Portfolio Optimization + """ + + self.po_parser = argparse.ArgumentParser(add_help=False, prog="po") + self.po_parser.add_argument("cmd", choices=self.CHOICES ) + self.tickers = set(tickers) + + @staticmethod + def print_help(tickers: Set[str]): + """Print help""" + print("\nPortfolio Optimization:") + print(" help show this menu again") + print( + " q quit this menu, and shows back to main menu" + ) + print(" quit quit to abandon program") + print( + f"\nCurrent Tickers: {('None', ', '.join(tickers))[bool(tickers)]}" + ) + print("") + print(" add add ticker to optimize") + print(" optimize run optimization") + print("") + + + def switch(self, an_input: str): + """Process and dispatch input + + Returns + ------- + True, False or None + False - quit the menu + True - quit the program + None - continue in the menu + """ + (known_args, other_args) = self.po_parser.parse_known_args(an_input.split()) + + return getattr( + self, "call_" + known_args.cmd, lambda: "Command not recognized!" + )(other_args) + + def call_help(self, _): + """Process Help command""" + self.print_help(self.tickers) + + def call_q(self, _): + """Process Q command - quit the menu""" + return False + + def call_quit(self, _): + """Process Quit command - quit the program""" + return True + + def call_add(self, other_args:List[str]): + self.add_stocks(self, other_args) + + def call_optimize(self, other_args:List[str]): + weights = po_api.optimize(self.tickers, other_args) + print("Optimal Weights for Equal Weighting:") + print(weights) + print("") + + @staticmethod + def add_stocks(self, other_args: List[str]): + + """ Add ticker to current list for optimization""" + parser = argparse.ArgumentParser( + add_help=False, + prog="add", + description="""Add tickers for optimizing.""", + ) + parser.add_argument( + "-t", + "--tickers", + dest="add_tickers", + type=lambda s: [str(item).upper() for item in s.split(",")], + default=[], + help="add tickers to optimzation.", + ) + try: + # For the case where a user uses: 'select NIO,XPEV,LI' + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-t") + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + for ticker in ns_parser.add_tickers: + self.tickers.add(ticker) + + + print( + f"\nCurrent Tickers: {('None', ', '.join(self.tickers))[bool(self.tickers)]}" + ) + print("") + except Exception as e: + print(e) + + print("") + + +def menu(tickers:List[str]): + """Portfolio Optimization Menu""" + if tickers == ['']: + tickers = [] + po_controller = PortfolioOptimization(tickers) + po_controller.call_help(tickers) + + while True: + # Get input command from user + if session and gtff.USE_PROMPT_TOOLKIT: + completer = NestedCompleter.from_nested_dict( + {c: None for c in po_controller.CHOICES} + ) + an_input = session.prompt( + f"{get_flair()} (po)> ", + completer=completer, + ) + else: + an_input = input(f"{get_flair()} (po)> ") + + try: + process_input = po_controller.switch(an_input) + + if process_input is not None: + return process_input + + except SystemExit: + print("The command selected doesn't exist\n") + continue + diff --git a/terminal.py b/terminal.py index 91fa02c2e94..123c73a78da 100644 --- a/terminal.py +++ b/terminal.py @@ -31,6 +31,7 @@ from gamestonk_terminal.residuals_analysis import ra_controller from gamestonk_terminal.portfolio import port_controller from gamestonk_terminal.cryptocurrency import crypto_controller from gamestonk_terminal.screener import screener_controller +from gamestonk_terminal.portfolio_optimization import port_optimization_controller as po_controller # import warnings # warnings.simplefilter("always") @@ -88,7 +89,9 @@ def main(): "pa", "crypto", "ra", + "po" ] + menu_parser.add_argument("opt", choices=choices) completer = NestedCompleter.from_nested_dict({c: None for c in choices}) @@ -242,6 +245,9 @@ def main(): elif ns_known_args.opt == "crypto": b_quit = crypto_controller.menu() + elif ns_known_args.opt == "po": + b_quit = po_controller.menu([s_ticker]) + elif ns_known_args.opt == "pred": if not gtff.ENABLE_PREDICT: From f2af23ded15d54d4afee93bce93db6d435e5f00a Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 11 Apr 2021 17:09:53 -0400 Subject: [PATCH 02/31] round1 edits --- .../comparison_analysis/ca_controller.py | 6 ++-- gamestonk_terminal/main_helper.py | 2 +- .../portfolio_optimization/port_opt_api.py | 31 ++++++++++++++++++- .../port_optimization_controller.py | 23 +++++++++++--- 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py index 9c4aaa89258..39329a01155 100644 --- a/gamestonk_terminal/comparison_analysis/ca_controller.py +++ b/gamestonk_terminal/comparison_analysis/ca_controller.py @@ -15,7 +15,7 @@ from gamestonk_terminal.comparison_analysis import yahoo_finance_api as yf_api from gamestonk_terminal.comparison_analysis import market_watch_api as mw_api from gamestonk_terminal.comparison_analysis import finbrain_api as f_api from gamestonk_terminal.comparison_analysis import finviz_compare_view -from gamestonk_terminal.portfolio_optimization import port_optimization_controller +from gamestonk_terminal.portfolio_optimization import port_optimization_controller as po_controller from gamestonk_terminal.menu import session from prompt_toolkit.completion import NestedCompleter @@ -109,7 +109,7 @@ class ComparisonAnalysisController: print(" performance brief performance comparison") print(" technical brief technical comparison") print("") - print(" po portfolio optimzation from selected tickers") + print(" >po portfolio optimzation from selected tickers") print("") return @@ -318,7 +318,7 @@ class ComparisonAnalysisController: def call_po(self, _): """Open Portfolio Optimization menu with ticker and similar""" - port_optimization_controller.menu([self.ticker] + self.similar) + return po_controller.menu([self.ticker] + self.similar) def menu(stock: pd.DataFrame, ticker: str, start: datetime, interval: str): diff --git a/gamestonk_terminal/main_helper.py b/gamestonk_terminal/main_helper.py index 8b152e09909..57289ec70f4 100644 --- a/gamestonk_terminal/main_helper.py +++ b/gamestonk_terminal/main_helper.py @@ -60,7 +60,7 @@ def print_help(s_ticker, s_start, s_interval, b_is_market_open): ) print(" pa portfolio analysis, \t\t supports: robinhood, alpaca, ally ") print(" crypto cryptocurrencies, \t\t uses coingecko api") - print(" po portfolio analysis, \t\t UPDATE ME BEFORE PR") + print(" po portfolio analysis, \t\t menu to explore portfolio optimizations") if s_ticker: print( diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_api.py b/gamestonk_terminal/portfolio_optimization/port_opt_api.py index c4654af9965..862f45dc0bc 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_api.py +++ b/gamestonk_terminal/portfolio_optimization/port_opt_api.py @@ -4,7 +4,7 @@ __docformat__ = "numpy" import argparse from typing import List -def optimize(list_of_stocks:List, other_args:List[str]): +def optimize(list_of_stocks:List[str], other_args:List[str]): """ Parameters @@ -18,6 +18,14 @@ def optimize(list_of_stocks:List, other_args:List[str]): Dictionary of weights where keys are the tickers """ + + parser = argparse.ArgumentParser( + add_help=False, + prog="optimize", + ) + + parse + weights = {} n_stocks = len(list_of_stocks) for stock in list_of_stocks: @@ -25,4 +33,25 @@ def optimize(list_of_stocks:List, other_args:List[str]): return weights +def equal_weight(list_of_stocks:List[str]): + """ + Equally weighted portfolio, where weight = 1/# of stocks + + Parameters + ---------- + list_of_stocks: List[str] + List of tickers to be included in optimization + + Returns + ------- + weights : dict + Dictionary of weights where keys are the tickers + + """ + weights = {} + n_stocks = len(list_of_stocks) + for stock in list_of_stocks: + weights[stock] = round(1 / n_stocks, 5) + + return weights diff --git a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py index b90bb4b8b8d..5892fb7f58c 100644 --- a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py +++ b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py @@ -17,9 +17,10 @@ class PortfolioOptimization: CHOICES = ["help", "q", "quit", - "clear", + "select", "add", - "optimize"] + "equal_weight", + "mkt_cap"] def __init__(self, tickers:Set[str] = None @@ -46,7 +47,11 @@ class PortfolioOptimization: ) print("") print(" add add ticker to optimize") - print(" optimize run optimization") + print(" select overwrite current tickers with new tickers") + print("") + print("Optimization:") + print(" equal_weight equally weighted portfolio") + print(" mkt_cap marketcap weighted portfolio") print("") @@ -86,6 +91,16 @@ class PortfolioOptimization: print("Optimal Weights for Equal Weighting:") print(weights) print("") + def call_equal_weight(self, other_args:List[str]): + weights = po_api.equal_weight(self.tickers, other_args) + print("Optimal Weights for Equal Weighting:") + print(weights) + print("") + + def call_mkt_cap(self,other_args:List[str]): + print("TODO") + def call_select(self,other_args:List[str]): + print("TODO") @staticmethod def add_stocks(self, other_args: List[str]): @@ -105,7 +120,7 @@ class PortfolioOptimization: help="add tickers to optimzation.", ) try: - # For the case where a user uses: 'select NIO,XPEV,LI' + if other_args: if "-" not in other_args[0]: other_args.insert(0, "-t") From abda4bf7e5fb76111a2f8a0a1354d24a24c90bcd Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 11 Apr 2021 17:57:51 -0400 Subject: [PATCH 03/31] added select/mktcap options --- .../portfolio_optimization/port_opt_api.py | 56 +++++++++---------- .../port_optimization_controller.py | 15 ++--- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_api.py b/gamestonk_terminal/portfolio_optimization/port_opt_api.py index 862f45dc0bc..7c7e8d9f3d4 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_api.py +++ b/gamestonk_terminal/portfolio_optimization/port_opt_api.py @@ -3,37 +3,9 @@ __docformat__ = "numpy" import argparse from typing import List +import yfinance as yf -def optimize(list_of_stocks:List[str], other_args:List[str]): - """ - - Parameters - ---------- - list_of_stocks: List[str] - List of tickers to be included in optimization - - Returns - ------- - weights : dict - Dictionary of weights where keys are the tickers - - """ - - parser = argparse.ArgumentParser( - add_help=False, - prog="optimize", - ) - - parse - - weights = {} - n_stocks = len(list_of_stocks) - for stock in list_of_stocks: - weights[stock] = round(1/n_stocks, 5) - - return weights - -def equal_weight(list_of_stocks:List[str]): +def equal_weight(list_of_stocks:List[str], other_args:List[str]): """ Equally weighted portfolio, where weight = 1/# of stocks @@ -55,3 +27,27 @@ def equal_weight(list_of_stocks:List[str]): return weights +def market_cap_weighting(list_of_stocks:List[str], other_args:List[str]): + """ + Market cap weighted portfolio where each weight is the stocks market cap/ sum of all market caps + Parameters + ---------- + list_of_stocks: List[str[ + List of tickers to be included in optimization + + Returns + ------- + weights: dict + Dictionary of weights where keys are the tickers + """ + weights = {} + mkt_cap = {} + total_cap = 0 + for stock in list_of_stocks: + stock_cap = yf.Ticker(stock).info["marketCap"] + mkt_cap[stock] = stock_cap + total_cap += stock_cap + for k,v in mkt_cap.items(): + weights[k] = round(v/total_cap,5) + + return weights diff --git a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py index 5892fb7f58c..aad015af3a5 100644 --- a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py +++ b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py @@ -86,11 +86,7 @@ class PortfolioOptimization: def call_add(self, other_args:List[str]): self.add_stocks(self, other_args) - def call_optimize(self, other_args:List[str]): - weights = po_api.optimize(self.tickers, other_args) - print("Optimal Weights for Equal Weighting:") - print(weights) - print("") + def call_equal_weight(self, other_args:List[str]): weights = po_api.equal_weight(self.tickers, other_args) print("Optimal Weights for Equal Weighting:") @@ -98,9 +94,14 @@ class PortfolioOptimization: print("") def call_mkt_cap(self,other_args:List[str]): - print("TODO") + weights = po_api.market_cap_weighting(self.tickers, other_args) + print("Market Cap Weighting Weights:") + print(weights) + print("") + def call_select(self,other_args:List[str]): - print("TODO") + self.tickers = set([]) + self.add_stocks(self, other_args) @staticmethod def add_stocks(self, other_args: List[str]): From 83066e8189a1774c2651743c1de725bfc29a698d Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 11 Apr 2021 19:39:07 -0400 Subject: [PATCH 04/31] update menus to allow switching between po/scr and po/ca. If going ca->po-> ca, the loaded stock remains in the ca menu. --- .../comparison_analysis/ca_controller.py | 5 +- gamestonk_terminal/main_helper.py | 2 +- .../port_optimization_controller.py | 55 ++++++++++++++++++- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py index 39329a01155..2e723970ff3 100644 --- a/gamestonk_terminal/comparison_analysis/ca_controller.py +++ b/gamestonk_terminal/comparison_analysis/ca_controller.py @@ -109,7 +109,7 @@ class ComparisonAnalysisController: print(" performance brief performance comparison") print(" technical brief technical comparison") print("") - print(" >po portfolio optimzation from selected tickers") + print(" > po portfolio optimization for selected tickers") print("") return @@ -318,8 +318,7 @@ class ComparisonAnalysisController: def call_po(self, _): """Open Portfolio Optimization menu with ticker and similar""" - return po_controller.menu([self.ticker] + self.similar) - + return po_controller.menu_from_ca(self.ticker, self.similar) def menu(stock: pd.DataFrame, ticker: str, start: datetime, interval: str): """Comparison Analysis Menu""" diff --git a/gamestonk_terminal/main_helper.py b/gamestonk_terminal/main_helper.py index 57289ec70f4..e09a7d2023c 100644 --- a/gamestonk_terminal/main_helper.py +++ b/gamestonk_terminal/main_helper.py @@ -60,7 +60,7 @@ def print_help(s_ticker, s_start, s_interval, b_is_market_open): ) print(" pa portfolio analysis, \t\t supports: robinhood, alpaca, ally ") print(" crypto cryptocurrencies, \t\t uses coingecko api") - print(" po portfolio analysis, \t\t menu to explore portfolio optimizations") + print(" po portfolio optimization, \t\t TBA") if s_ticker: print( diff --git a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py index aad015af3a5..18523068090 100644 --- a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py +++ b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py @@ -5,11 +5,14 @@ __docformat__ = "numpy" import argparse from typing import List, Set from datetime import datetime - +import pandas as pd 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.comparison_analysis import ca_controller +from gamestonk_terminal.screener import screener_controller + from prompt_toolkit.completion import NestedCompleter class PortfolioOptimization: @@ -17,6 +20,8 @@ class PortfolioOptimization: CHOICES = ["help", "q", "quit", + "ca", + "scr", "select", "add", "equal_weight", @@ -32,6 +37,9 @@ class PortfolioOptimization: self.po_parser = argparse.ArgumentParser(add_help=False, prog="po") self.po_parser.add_argument("cmd", choices=self.CHOICES ) self.tickers = set(tickers) + # These will allow the ca menu to be re-access + self.ca_ticker = None + self.ca_similar = None @staticmethod def print_help(tickers: Set[str]): @@ -42,6 +50,8 @@ class PortfolioOptimization: " q quit this menu, and shows back to main menu" ) print(" quit quit to abandon program") + print(" > ca comparison analysis menu") + print(" > scr screener menu") print( f"\nCurrent Tickers: {('None', ', '.join(tickers))[bool(tickers)]}" ) @@ -83,10 +93,16 @@ class PortfolioOptimization: """Process Quit command - quit the program""" return True + def call_ca(self, other_args:List[str]): + + return ca_controller.menu(pd.DataFrame(), self.ca_ticker, "", "1440min") + + def call_scr(self, _): + return screener_controller.menu() + def call_add(self, other_args:List[str]): 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:") @@ -143,6 +159,41 @@ class PortfolioOptimization: print("") + @classmethod + def from_ca_menu(cls, ticker: str, similar: List[str]): + return cls(set([ticker] + similar)) + + +def menu_from_ca(ticker:str, similar:List[str]): + """Portfolio Optimization Menu from ca menu that allows for jumping between""" + po_controller = PortfolioOptimization.from_ca_menu(ticker,similar) + po_controller.ca_ticker = ticker + po_controller.ca_similar = similar + po_controller.call_help([ticker]+ similar) + + while True: + # Get input command from user + if session and gtff.USE_PROMPT_TOOLKIT: + completer = NestedCompleter.from_nested_dict( + {c: None for c in po_controller.CHOICES} + ) + an_input = session.prompt( + f"{get_flair()} (po)> ", + completer=completer, + ) + else: + an_input = input(f"{get_flair()} (po)> ") + + try: + process_input = po_controller.switch(an_input) + + if process_input is not None: + return process_input + + except SystemExit: + print("The command selected doesn't exist\n") + continue + def menu(tickers:List[str]): """Portfolio Optimization Menu""" From f8e4d06de9d7d2ddcbc20bb44853e4db063d57b9 Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 11 Apr 2021 20:27:54 -0400 Subject: [PATCH 05/31] poetry add + requirement adds --- .../portfolio_optimization/port_opt_api.py | 3 + .../port_optimization_controller.py | 7 +- poetry.lock | 173 +++++++++++++++++- pyproject.toml | 1 + requirements-full.txt | 1 + requirements-old.txt | 1 + requirements-win.txt | 1 + requirements.txt | 1 + 8 files changed, 181 insertions(+), 7 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_api.py b/gamestonk_terminal/portfolio_optimization/port_opt_api.py index 7c7e8d9f3d4..97c62979e70 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_api.py +++ b/gamestonk_terminal/portfolio_optimization/port_opt_api.py @@ -4,6 +4,9 @@ __docformat__ = "numpy" import argparse from typing import List import yfinance as yf +from pypfopt.efficient_frontier import EfficientFrontier +from pypfopt import risk_models +from pypfopt import expected_returns def equal_weight(list_of_stocks:List[str], other_args:List[str]): """ diff --git a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py index 18523068090..ef7c2d1a128 100644 --- a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py +++ b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py @@ -60,9 +60,12 @@ class PortfolioOptimization: print(" select overwrite current tickers with new tickers") print("") print("Optimization:") - print(" equal_weight equally weighted portfolio") - print(" mkt_cap marketcap weighted portfolio") print("") + print(" Property weighted:") + print(" equal_weight equally weighted portfolio") + print(" mkt_cap marketcap weighted portfolio") + print(" Mean Variance Optimization") + print(" max_sharpe portfolio with maximum sharpe ratio") def switch(self, an_input: str): diff --git a/poetry.lock b/poetry.lock index b20746d369b..5cbefaf1f5d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -379,6 +379,21 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "cvxpy" +version = "1.1.12" +description = "A domain-specific language for modeling convex optimization problems in Python." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +ecos = ">=2" +numpy = ">=1.15" +osqp = ">=0.4.1" +scipy = ">=1.1.0" +scs = ">=1.1.6" + [[package]] name = "cycler" version = "0.10.0" @@ -456,6 +471,18 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "ecos" +version = "2.0.7.post1" +description = "This is the Python package for ECOS: Embedded Cone Solver. See Github page for more information." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +numpy = ">=1.6" +scipy = ">=0.9" + [[package]] name = "entrypoints" version = "0.3" @@ -1491,6 +1518,19 @@ numpy = ">=1.7" docs = ["sphinx (==1.2.3)", "sphinxcontrib-napoleon", "sphinx-rtd-theme", "numpydoc"] tests = ["pytest", "pytest-cov", "pytest-pep8"] +[[package]] +name = "osqp" +version = "0.6.2.post0" +description = "OSQP: The Operator Splitting QP Solver" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +numpy = ">=1.7" +qdldl = "*" +scipy = ">=0.13.2" + [[package]] name = "overrides" version = "3.1.0" @@ -1930,6 +1970,23 @@ category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "pyportfolioopt" +version = "1.4.1" +description = "Financial portfolio optimization in python" +category = "main" +optional = false +python-versions = ">=3.6.1,<4.0.0" + +[package.dependencies] +cvxpy = ">=1.1.10,<2.0.0" +numpy = ">=1.12,<2.0" +pandas = ">=0.19" +scipy = ">=1.3,<2.0" + +[package.extras] +optionals = ["cvxopt (>=1.2,!=1.2.5.post1,<2.0)", "scikit-learn (>=0.24.1,<0.25.0)", "matplotlib (>=3.2.0,<4.0.0)"] + [[package]] name = "pyrsistent" version = "0.14.11" @@ -2073,6 +2130,18 @@ python-versions = ">=3.6" cffi = {version = "*", markers = "implementation_name == \"pypy\""} py = {version = "*", markers = "implementation_name == \"pypy\""} +[[package]] +name = "qdldl" +version = "0.1.5.post0" +description = "QDLDL, a free LDL factorization routine." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +numpy = ">=1.7" +scipy = ">=0.13.2" + [[package]] name = "qtconsole" version = "5.0.3" @@ -2265,6 +2334,18 @@ Cython = {version = "*", markers = "sys_platform == \"darwin\""} dataclasses = {version = "*", markers = "python_version < \"3.7\""} pyobjc-framework-Cocoa = {version = "*", markers = "sys_platform == \"darwin\""} +[[package]] +name = "scs" +version = "2.1.2" +description = "scs: splitting conic solver" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +numpy = ">=1.7" +scipy = ">=0.13.2" + [[package]] name = "seaborn" version = "0.11.1" @@ -2837,7 +2918,7 @@ prediction = ["fbprophet", "tensorflow", "pmdarima", "transformers", "flair", "t [metadata] lock-version = "1.1" python-versions = "^3.6.8" -content-hash = "144771641ef753b1e937c0db16496c409905f6f692ef81a03d9ba716d9207bb0" +content-hash = "5b3abe79834fc75092d1d9e8e3a809252f56eddc170fb61a93c36ffef9328b73" [metadata.files] absl-py = [ @@ -3063,6 +3144,20 @@ cssselect = [ {file = "cssselect-1.1.0-py2.py3-none-any.whl", hash = "sha256:f612ee47b749c877ebae5bb77035d8f4202c6ad0f0fc1271b3c18ad6c4468ecf"}, {file = "cssselect-1.1.0.tar.gz", hash = "sha256:f95f8dedd925fd8f54edb3d2dfb44c190d9d18512377d3c1e2388d16126879bc"}, ] +cvxpy = [ + {file = "cvxpy-1.1.12-cp36-cp36m-win_amd64.whl", hash = "sha256:86f8e4aa1a79482431b89aa526249d9fda04c74e04a3ec224d1175d281643d71"}, + {file = "cvxpy-1.1.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:90b09a7361b9c614166fa673e1fe9321317df6939135330e1621b9a0bf10e3a8"}, + {file = "cvxpy-1.1.12-cp37-cp37m-win_amd64.whl", hash = "sha256:084e5292f2a13133a1cd28d68f5af1df33a5119b0f1aec7cecf834eea3e2d11d"}, + {file = "cvxpy-1.1.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:575f6e1c70274008961edff97a7494701faed20b40fbd49327010c0e7d186256"}, + {file = "cvxpy-1.1.12-cp38-cp38-win_amd64.whl", hash = "sha256:76d04444cb50ad2840bbc72a625ac246b5eeeeb580ee20c71bec057822667738"}, + {file = "cvxpy-1.1.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2de5e0e75316839bc71b5df2c2db0131cee7ef06dac566c6c44dc852c091502b"}, + {file = "cvxpy-1.1.12-cp39-cp39-win_amd64.whl", hash = "sha256:9fa4d033ab31855aea090e4b358017b36880390a7d0e6e97ecc5226c77eab489"}, + {file = "cvxpy-1.1.12-py3.6-win-amd64.egg", hash = "sha256:d2c28e0eea1b6dc70262c9ed8b63c5c1747d85c28b3ec24cbaab07583b3ad729"}, + {file = "cvxpy-1.1.12-py3.7-win-amd64.egg", hash = "sha256:44d0e3d60693831ac70bdc9b2e23faf84401347bfcdef4eb5da5ae22ac57f5ff"}, + {file = "cvxpy-1.1.12-py3.8-win-amd64.egg", hash = "sha256:c86f8de9a63ccbd45844a4482df8099824688b5329060f089aad1c3beb939190"}, + {file = "cvxpy-1.1.12-py3.9-win-amd64.egg", hash = "sha256:08c720706abd94c25903a4db5dd2171232e72d0969b874a5de9a8181f96357d5"}, + {file = "cvxpy-1.1.12.tar.gz", hash = "sha256:b499ebfae4fc645e9523621573ffcb1c5b6854a1b5c0ce1d66a8a9a458605807"}, +] cycler = [ {file = "cycler-0.10.0-py2.py3-none-any.whl", hash = "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d"}, {file = "cycler-0.10.0.tar.gz", hash = "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"}, @@ -3126,6 +3221,30 @@ detecta = [ {file = "detecta-0.0.5-py3-none-any.whl", hash = "sha256:dbaf1938e5d785386c904034e2553824e2aa31793e967984ca65983aca06d23e"}, {file = "detecta-0.0.5.tar.gz", hash = "sha256:d2ea7d13dfbbc994d6ce385a7f8dc0a85fe675a8a8e712a64ec56e54c40603ed"}, ] +ecos = [ + {file = "ecos-2.0.7.post1-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:dd9f01e28fe58894fb394931804884122606fb4e2a59d4514b803e9cd11b7d2b"}, + {file = "ecos-2.0.7.post1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:db7433051f6072d4821ebc582e9ff853d7d631ed98770550d248eae70b29dd26"}, + {file = "ecos-2.0.7.post1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:96ddc1c4e440820bb343c44785da480a64a9c468ec467b997e40f0c7d3236226"}, + {file = "ecos-2.0.7.post1-cp27-cp27m-win32.whl", hash = "sha256:574fa26661d192e48551a30e217296875020fdb7eebf9a072a14d68a0b9de03a"}, + {file = "ecos-2.0.7.post1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7c871b7a49a5855e7df0d60a06d0e2148a4b183d612f7db5e155988629c3ec21"}, + {file = "ecos-2.0.7.post1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:845455f99cd579ee0cdfbcea675b4e4f7674b563e6a54a225977fc0819cda7e0"}, + {file = "ecos-2.0.7.post1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:e002df0f4b6777be68c73756e60f3cf76cb5f2f7d36c4f1a482c5538aac1a287"}, + {file = "ecos-2.0.7.post1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:feda86ddd191b1ae34d5ea615743894d1baa800f2dbb552cb7c7095a87037831"}, + {file = "ecos-2.0.7.post1-cp34-cp34m-win32.whl", hash = "sha256:6b0829b76ba49f6ebf8b9512673a0b702b756704927cbb106043e1b2273dadea"}, + {file = "ecos-2.0.7.post1-cp34-cp34m-win_amd64.whl", hash = "sha256:54f7c480029fbfa738ddb8e538388868b2a17c9189a426d924ca0c36e4cbbbd5"}, + {file = "ecos-2.0.7.post1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1f811e7244a58a7037474b3dead85c7ffd524cf631c90584cc544e438f114cf9"}, + {file = "ecos-2.0.7.post1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:72657189f71dbde01d1df841d2139e04da17071c8d3270919c8501f239d8c8e4"}, + {file = "ecos-2.0.7.post1-cp35-cp35m-win32.whl", hash = "sha256:30e7e9c5ad8a012ba1f69aa6827beb99f208323f678a82fcccc30e4ab8090aad"}, + {file = "ecos-2.0.7.post1-cp35-cp35m-win_amd64.whl", hash = "sha256:94dd0f82a18550232e4924e6c42730c46d7cdc03c4e2dc889e98ec97c0f24061"}, + {file = "ecos-2.0.7.post1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:361758a3568eb5a3a9b37c00a22b36f9548bbd0cd21f1da904a3627285bb4274"}, + {file = "ecos-2.0.7.post1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:831acb6bac205025ffe87002d8f425d2764a70db3d9d053a1f7e0e50bc2a18b9"}, + {file = "ecos-2.0.7.post1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:912b17f72476aff33b32e92decd7c02ddb929db28227563b3750948783cff6f4"}, + {file = "ecos-2.0.7.post1-cp36-cp36m-win32.whl", hash = "sha256:4fdfee011853cd07d494ef58a9299b1f0ccf0268c375109cb2b60727f746ea74"}, + {file = "ecos-2.0.7.post1-cp36-cp36m-win_amd64.whl", hash = "sha256:fb64fb29aef26474f807df4f0c198a6d192291edc9faa3bb05e3bc9b1e2b960a"}, + {file = "ecos-2.0.7.post1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4b3068ef023c4f39f6c0d6fd73d27d8a8008f8b14e1f4ddd8d0f4876e841b986"}, + {file = "ecos-2.0.7.post1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4f775caf828597d094cddae54f7ed12b4a5aa760f535a92ccabec47741e4d61c"}, + {file = "ecos-2.0.7.post1.tar.gz", hash = "sha256:83e90f42b3f32e2a93f255c3cfad2da78dbd859119e93844c45d2fca20bdc758"}, +] entrypoints = [ {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, @@ -3810,6 +3929,29 @@ opt-einsum = [ {file = "opt_einsum-3.3.0-py3-none-any.whl", hash = "sha256:2455e59e3947d3c275477df7f5205b30635e266fe6dc300e3d9f9646bfcea147"}, {file = "opt_einsum-3.3.0.tar.gz", hash = "sha256:59f6475f77bbc37dcf7cd748519c0ec60722e91e63ca114e68821c0c54a46549"}, ] +osqp = [ + {file = "osqp-0.6.2.post0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:21047e9ceb4c2224ba15dd95f5125e2dcabd178aa93d8efa407e67216ea4fe62"}, + {file = "osqp-0.6.2.post0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:598a7bb1534a0ab69bcaf22d6e6c4ec042a41184f007c01b7fa863b528c6b61d"}, + {file = "osqp-0.6.2.post0-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:6b745ba55add8be63eaee059858b7c0cf59925074fcc8355c17c3863fad01a95"}, + {file = "osqp-0.6.2.post0-cp35-cp35m-win_amd64.whl", hash = "sha256:1104dd56d6bfe5ac5e5e076e1cf9e2be76932cadae4e0cfbdf495f3f43e4f8fa"}, + {file = "osqp-0.6.2.post0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6f89feaf7c30dfb5031b370e8c45aed23a60d6471606bad9b7d23506ddf807e6"}, + {file = "osqp-0.6.2.post0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:198b66cbe9326ef181f7598856aae20dcc5b7824b119cc119b412206e858bc28"}, + {file = "osqp-0.6.2.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9aaecd3ed00a99d063e5bd45ebce965d1ebc8f4754d6c05ab5d31c3577a68513"}, + {file = "osqp-0.6.2.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:2d150d880dc5bee015aadac6cff32ab0c2721cd4bb8a3f5b1f0cec819fe70b39"}, + {file = "osqp-0.6.2.post0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:20cd6a352694cc182bb79a8e19e4e03b7970ecb4b95d6abf86e3c578a45783a9"}, + {file = "osqp-0.6.2.post0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:96e42524dd156f62ac8e35d71831588acb2919b5ba27e0e695c757539708dfa5"}, + {file = "osqp-0.6.2.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:d6dd2b70ffcee6089758518620087a5909a10621a59493be8ce0ef2a74502a27"}, + {file = "osqp-0.6.2.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:7c9f7e661fe2da984eab6533a56b5366f10a7360a28b8dd18c222fcf6655e247"}, + {file = "osqp-0.6.2.post0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b102c26b2017b039032797a388548dcfe11dbd3d06b3294721fa65976fe1065e"}, + {file = "osqp-0.6.2.post0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a835746179c81944e51a6a2e0efb0aca66402be351af912bfd682527f15a35a7"}, + {file = "osqp-0.6.2.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:57f5026b91bb8f39059576c5ee50c0a1c2705d775a776e2e24be2aa47e63d8b0"}, + {file = "osqp-0.6.2.post0-cp38-cp38-win_amd64.whl", hash = "sha256:fb0d77e634362fe59ab2bedba3dde40d5357922e9719bcb4d16a99766cefd5a2"}, + {file = "osqp-0.6.2.post0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e21463d97bcab58350a41a975e9e5e4a685087b6357899a58813d2238795315"}, + {file = "osqp-0.6.2.post0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:02fdbabfffd72a8118328d117a83998800ca015182053fced9d5e8be6ffd702a"}, + {file = "osqp-0.6.2.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:b238b9049a29f6955f08567b285b0734dd578b529a3780d617854ffd91e5e4ed"}, + {file = "osqp-0.6.2.post0-cp39-cp39-win_amd64.whl", hash = "sha256:2c6cb14fd1365004d0e7a01f6101e16414a0348db736ee45aac8865fac165e57"}, + {file = "osqp-0.6.2.post0.tar.gz", hash = "sha256:5f0695f26a3bef0fae91254bc283fab790dcca0064bfe0f425167f9c9e8b4cbc"}, +] overrides = [ {file = "overrides-3.1.0.tar.gz", hash = "sha256:30f761124579e59884b018758c4d7794914ef02a6c038621123fec49ea7599c6"}, ] @@ -4069,6 +4211,10 @@ pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] +pyportfolioopt = [ + {file = "PyPortfolioOpt-1.4.1-py3-none-any.whl", hash = "sha256:73f0b4ac2a5d879879a7a91af038b4d7ff82e9998fb2cad7c249302294e8c09e"}, + {file = "PyPortfolioOpt-1.4.1.tar.gz", hash = "sha256:e6c8e983db92cd13caf41d87fb4dcd945d37053f28b75dd19b5be699b6b1d19e"}, +] pyrsistent = [ {file = "pyrsistent-0.14.11.tar.gz", hash = "sha256:3ca82748918eb65e2d89f222b702277099aca77e34843c5eb9d52451173970e2"}, ] @@ -4212,6 +4358,24 @@ pyzmq = [ {file = "pyzmq-22.0.3-pp37-pypy37_pp73-win32.whl", hash = "sha256:279cc9b51db48bec2db146f38e336049ac5a59e5f12fb3a8ad864e238c1c62e3"}, {file = "pyzmq-22.0.3.tar.gz", hash = "sha256:f7f63ce127980d40f3e6a5fdb87abf17ce1a7c2bd8bf2c7560e1bbce8ab1f92d"}, ] +qdldl = [ + {file = "qdldl-0.1.5.post0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:787d59b4301608e96bdf32ab3a572d9f41b3ea08581774826720986e18da261e"}, + {file = "qdldl-0.1.5.post0-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:223dd49f4aa625c727c3053e9f62814fab2f929e0575b34dd1f38e66b536a849"}, + {file = "qdldl-0.1.5.post0-cp35-cp35m-win_amd64.whl", hash = "sha256:cd0090d5512a8e386534a755f9ebd8be82087d401746cebbfb9f9faa21073c39"}, + {file = "qdldl-0.1.5.post0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3e5e2ee6b45dd655120ca8d6645331c800c494ce765bc0674714ba86e1ebec7d"}, + {file = "qdldl-0.1.5.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:a2ab0d4618d5516101fc0ce99c873df61a085c3d7cb4d8347d5eca818795e48f"}, + {file = "qdldl-0.1.5.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:ac7a2891628554de6260132fa090c5b00cf32ae083ece4991a43b887ec6f2114"}, + {file = "qdldl-0.1.5.post0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:817a226b2450e56c50f61a92b727c69f50a4d043876e5f1bcdff096c5a82bc37"}, + {file = "qdldl-0.1.5.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:c10f91dda6a422a6d879668fd293132c8939431fffe64abd9700dce170a3aec4"}, + {file = "qdldl-0.1.5.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:e70c57ea65d0bb09708bc9ecafd4fb4b797e64af4da475404e6bcf5a2cc92596"}, + {file = "qdldl-0.1.5.post0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:64f531ef2a12cc77be85045cdffdb2fe339244c73fc07aebe49977541bfc7e79"}, + {file = "qdldl-0.1.5.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:cff34b89258a3c9b1b398f1c8eeb480a9c4ae2ba14c4ffc8c78cdb123dab11c6"}, + {file = "qdldl-0.1.5.post0-cp38-cp38-win_amd64.whl", hash = "sha256:3a7ec584aaddff7036d22013c911132572f3d59c65ec6bf7e32beb01765accc4"}, + {file = "qdldl-0.1.5.post0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a7c086ae21a2b2e43b31ac6a5024c79dadf16e7c3152eda9e3263cefe1675e26"}, + {file = "qdldl-0.1.5.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:2c09f4b1a1c6f3a0579af004443417e084491e7c844ff9fb73170bb5d43f70b5"}, + {file = "qdldl-0.1.5.post0-cp39-cp39-win_amd64.whl", hash = "sha256:640cab781f87c2f1c04ada65615b3d1d136d319c4595176f37e2e3c99149108f"}, + {file = "qdldl-0.1.5.post0.tar.gz", hash = "sha256:c392c7427651d8b226423c7aba4a0f2338a1f38a4bbdabac6bc4afd8bc934f06"}, +] qtconsole = [ {file = "qtconsole-5.0.3-py3-none-any.whl", hash = "sha256:4a38053993ca2da058f76f8d75b3d8906efbf9183de516f92f222ac8e37d9614"}, {file = "qtconsole-5.0.3.tar.gz", hash = "sha256:c091a35607d2a2432e004c4a112d241ce908086570cf68594176dd52ccaa212d"}, @@ -4359,7 +4523,6 @@ scikit-learn = [ {file = "scikit_learn-0.24.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c658432d8a20e95398f6bb95ff9731ce9dfa343fdf21eea7ec6a7edfacd4b4d9"}, {file = "scikit_learn-0.24.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9dfa564ef27e8e674aa1cc74378416d580ac4ede1136c13dd555a87996e13422"}, {file = "scikit_learn-0.24.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:9c6097b6a9b2bafc5e0f31f659e6ab5e131383209c30c9e978c5b8abdac5ed2a"}, - {file = "scikit_learn-0.24.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5580eba7345a4d3b097be2f067cc71a306c44bab19e8717a30361f279c929bea"}, {file = "scikit_learn-0.24.1-cp36-cp36m-win32.whl", hash = "sha256:7b04691eb2f41d2c68dbda8d1bd3cb4ef421bdc43aaa56aeb6c762224552dfb6"}, {file = "scikit_learn-0.24.1-cp36-cp36m-win_amd64.whl", hash = "sha256:1adf483e91007a87171d7ce58c34b058eb5dab01b5fee6052f15841778a8ecd8"}, {file = "scikit_learn-0.24.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:ddb52d088889f5596bc4d1de981f2eca106b58243b6679e4782f3ba5096fd645"}, @@ -4367,7 +4530,6 @@ scikit-learn = [ {file = "scikit_learn-0.24.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0567a2d29ad08af98653300c623bd8477b448fe66ced7198bef4ed195925f082"}, {file = "scikit_learn-0.24.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:99349d77f54e11f962d608d94dfda08f0c9e5720d97132233ebdf35be2858b2d"}, {file = "scikit_learn-0.24.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:83b21ff053b1ff1c018a2d24db6dd3ea339b1acfbaa4d9c881731f43748d8b3b"}, - {file = "scikit_learn-0.24.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:abe835a851610f87201819cb315f8d554e1a3e8128912783a31e87264ba5ffb7"}, {file = "scikit_learn-0.24.1-cp37-cp37m-win32.whl", hash = "sha256:c3deb3b19dd9806acf00cf0d400e84562c227723013c33abefbbc3cf906596e9"}, {file = "scikit_learn-0.24.1-cp37-cp37m-win_amd64.whl", hash = "sha256:d54dbaadeb1425b7d6a66bf44bee2bb2b899fe3e8850b8e94cfb9c904dcb46d0"}, {file = "scikit_learn-0.24.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:3c4f07f47c04e81b134424d53c3f5e16dfd7f494e44fd7584ba9ce9de2c5e6c1"}, @@ -4375,7 +4537,6 @@ scikit-learn = [ {file = "scikit_learn-0.24.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:4ddd2b6f7449a5d539ff754fa92d75da22de261fd8fdcfb3596799fadf255101"}, {file = "scikit_learn-0.24.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:826b92bf45b8ad80444814e5f4ac032156dd481e48d7da33d611f8fe96d5f08b"}, {file = "scikit_learn-0.24.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:259ec35201e82e2db1ae2496f229e63f46d7f1695ae68eef9350b00dc74ba52f"}, - {file = "scikit_learn-0.24.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:9599a3f3bf33f73fed0fe06d1dfa4e6081365a58c1c807acb07271be0dce9733"}, {file = "scikit_learn-0.24.1-cp38-cp38-win32.whl", hash = "sha256:8772b99d683be8f67fcc04789032f1b949022a0e6880ee7b75a7ec97dbbb5d0b"}, {file = "scikit_learn-0.24.1-cp38-cp38-win_amd64.whl", hash = "sha256:ed9d65594948678827f4ff0e7ae23344e2f2b4cabbca057ccaed3118fdc392ca"}, {file = "scikit_learn-0.24.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:8aa1b3ac46b80eaa552b637eeadbbce3be5931e4b5002b964698e33a1b589e1e"}, @@ -4383,7 +4544,6 @@ scikit-learn = [ {file = "scikit_learn-0.24.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:087dfede39efb06ab30618f9ab55a0397f29c38d63cd0ab88d12b500b7d65fd7"}, {file = "scikit_learn-0.24.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:895dbf2030aa7337649e36a83a007df3c9811396b4e2fa672a851160f36ce90c"}, {file = "scikit_learn-0.24.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:9a24d1ccec2a34d4cd3f2a1f86409f3f5954cc23d4d2270ba0d03cf018aa4780"}, - {file = "scikit_learn-0.24.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:54be0a60a5a35005ad69c75902e0f5c9f699db4547ead427e97ef881c3242e6f"}, {file = "scikit_learn-0.24.1-cp39-cp39-win32.whl", hash = "sha256:fab31f48282ebf54dd69f6663cd2d9800096bad1bb67bbc9c9ac84eb77b41972"}, {file = "scikit_learn-0.24.1-cp39-cp39-win_amd64.whl", hash = "sha256:4562dcf4793e61c5d0f89836d07bc37521c3a1889da8f651e2c326463c4bd697"}, ] @@ -4417,6 +4577,9 @@ scipy = [ screeninfo = [ {file = "screeninfo-0.6.7.tar.gz", hash = "sha256:1c4bac1ca329da3f68cbc4d2fbc92256aa9bb8ff8583ee3e14f91f0a7baa69cb"}, ] +scs = [ + {file = "scs-2.1.2.tar.gz", hash = "sha256:667ed6019bb4e2f925bd9291161d2c61cc0077443094437b703ea905333fd585"}, +] seaborn = [ {file = "seaborn-0.11.1-py3-none-any.whl", hash = "sha256:4e1cce9489449a1c6ff3c567f2113cdb41122f727e27a984950d004a88ef3c5c"}, {file = "seaborn-0.11.1.tar.gz", hash = "sha256:44e78eaed937c5a87fc7a892c329a7cc091060b67ebd1d0d306b446a74ba01ad"}, diff --git a/pyproject.toml b/pyproject.toml index 5895d2c9dd0..1082086197e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ detecta = "^0.0.5" tradingview-ta = "^3.2.3" finvizfinance = "^0.9.3" Pillow = "^8.2.0" +pyportfolioopt = "^1.4.1" [tool.poetry.dev-dependencies] pytest = "^6.2.2" diff --git a/requirements-full.txt b/requirements-full.txt index 2bdd5e071dc..1c51e43294a 100644 --- a/requirements-full.txt +++ b/requirements-full.txt @@ -133,6 +133,7 @@ pycparser==2.20; python_version >= "3.5" and python_full_version < "3.0.0" or py pygments==2.8.1; python_version >= "3.6" pymeeus==0.5.9; python_version >= "3.5" pyparsing==2.4.7; python_full_version >= "3.6.0" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6") +pyportfolioopt==1.4.1 pyrsistent==0.17.3; python_version >= "3.6" pysocks==1.7.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" pystan==2.19.1.1 diff --git a/requirements-old.txt b/requirements-old.txt index f06595a2ee6..3a4b6bb05c4 100644 --- a/requirements-old.txt +++ b/requirements-old.txt @@ -90,6 +90,7 @@ pyasn1==0.4.8; python_version >= "3.5" and python_full_version < "3.0.0" and pyt pycoingecko==1.4.0 pymeeus==0.4.3; python_version >= "3.5" pyparsing==2.4.7; python_full_version >= "3.6.0" and python_version >= "3.6" +pyportfolioopt==1.4.1 pysocks==1.7.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" pystan==2.18 python-dateutil==2.8.1; python_full_version >= "3.6.1" and python_version >= "3.5" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0") and (python_version >= "2.7" and python_full_version < "3.0.0" and python_version < "4" or python_version >= "2.7" and python_version < "4" and python_full_version >= "3.3.0") diff --git a/requirements-win.txt b/requirements-win.txt index a5198652fef..560a2508261 100644 --- a/requirements-win.txt +++ b/requirements-win.txt @@ -92,6 +92,7 @@ pyasn1==0.4.8; python_version >= "3.5" and python_full_version < "3.0.0" and pyt pycoingecko==1.4.0 pymeeus==0.4.3; python_version >= "3.5" pyparsing==2.4.7; python_full_version >= "3.6.0" and python_version >= "3.6" +pyportfolioopt==1.4.1 pysocks==1.7.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" pystan==2.19.1.1 python-dateutil==2.8.1; python_full_version >= "3.6.1" and python_version >= "3.5" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0") and (python_version >= "2.7" and python_full_version < "3.0.0" and python_version < "4" or python_version >= "2.7" and python_version < "4" and python_full_version >= "3.3.0") diff --git a/requirements.txt b/requirements.txt index b89711f769c..4f4ba01310a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -92,6 +92,7 @@ pycparser==2.20; python_version >= "3.5" and python_full_version < "3.0.0" or py pygments==2.8.1; python_version >= "3.6" pymeeus==0.5.9; python_version >= "3.5" pyparsing==2.4.7; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +pyportfolioopt==1.4.1 pyrsistent==0.17.3; python_version >= "3.6" pysocks==1.7.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" python-dateutil==2.8.1; python_full_version >= "3.6.1" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0") From 811f56887573353e1980dac6e04569ca2108ef03 Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 11 Apr 2021 21:07:13 -0400 Subject: [PATCH 06/31] added max sharpe + black --- .../comparison_analysis/ca_controller.py | 7 +- .../portfolio_optimization/port_opt_api.py | 79 +++++++++++++----- .../port_optimization_controller.py | 83 ++++++++++--------- 3 files changed, 109 insertions(+), 60 deletions(-) diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py index 2e723970ff3..7db095a4038 100644 --- a/gamestonk_terminal/comparison_analysis/ca_controller.py +++ b/gamestonk_terminal/comparison_analysis/ca_controller.py @@ -15,7 +15,9 @@ from gamestonk_terminal.comparison_analysis import yahoo_finance_api as yf_api from gamestonk_terminal.comparison_analysis import market_watch_api as mw_api from gamestonk_terminal.comparison_analysis import finbrain_api as f_api from gamestonk_terminal.comparison_analysis import finviz_compare_view -from gamestonk_terminal.portfolio_optimization import port_optimization_controller as po_controller +from gamestonk_terminal.portfolio_optimization import ( + port_optimization_controller as po_controller, +) from gamestonk_terminal.menu import session from prompt_toolkit.completion import NestedCompleter @@ -43,7 +45,7 @@ class ComparisonAnalysisController: "ownership", "performance", "technical", - "po" + "po", ] def __init__( @@ -320,6 +322,7 @@ class ComparisonAnalysisController: """Open Portfolio Optimization menu with ticker and similar""" return po_controller.menu_from_ca(self.ticker, self.similar) + def menu(stock: pd.DataFrame, ticker: str, start: datetime, interval: str): """Comparison Analysis Menu""" diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_api.py b/gamestonk_terminal/portfolio_optimization/port_opt_api.py index 97c62979e70..71e697b73cc 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_api.py +++ b/gamestonk_terminal/portfolio_optimization/port_opt_api.py @@ -3,26 +3,28 @@ __docformat__ = "numpy" import argparse from typing import List +import pandas as pd import yfinance as yf from pypfopt.efficient_frontier import EfficientFrontier from pypfopt import risk_models from pypfopt import expected_returns -def equal_weight(list_of_stocks:List[str], other_args:List[str]): + +def equal_weight(list_of_stocks: List[str], other_args: List[str]): """ - Equally weighted portfolio, where weight = 1/# of stocks + Equally weighted portfolio, where weight = 1/# of stocks - Parameters - ---------- - list_of_stocks: List[str] - List of tickers to be included in optimization + Parameters + ---------- + list_of_stocks: List[str] + List of tickers to be included in optimization - Returns - ------- - weights : dict - Dictionary of weights where keys are the tickers + Returns + ------- + weights : dict + Dictionary of weights where keys are the tickers - """ + """ weights = {} n_stocks = len(list_of_stocks) for stock in list_of_stocks: @@ -30,13 +32,19 @@ def equal_weight(list_of_stocks:List[str], other_args:List[str]): return weights -def market_cap_weighting(list_of_stocks:List[str], other_args:List[str]): + +def property_weighting( + list_of_stocks: List[str], property_type: str, other_args: List[str] +): """ - Market cap weighted portfolio where each weight is the stocks market cap/ sum of all market caps + Property weighted portfolio where each weight is the relative fraction. Examples Parameters ---------- - list_of_stocks: List[str[ + list_of_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 Returns ------- @@ -44,13 +52,42 @@ def market_cap_weighting(list_of_stocks:List[str], other_args:List[str]): Dictionary of weights where keys are the tickers """ weights = {} - mkt_cap = {} - total_cap = 0 + prop = {} + prop_sum = 0 + for stock in list_of_stocks: - stock_cap = yf.Ticker(stock).info["marketCap"] - mkt_cap[stock] = stock_cap - total_cap += stock_cap - for k,v in mkt_cap.items(): - weights[k] = round(v/total_cap,5) + stock_prop = yf.Ticker(stock).info["marketCap"] + prop[stock] = stock_prop + prop_sum += stock_prop + for k, v in prop.items(): + weights[k] = round(v / prop_sum, 5) return weights + + +def max_sharpe(list_of_stocks: List[str], other_args: List[str]): + """ + Return a portfolio that maximizes the sharpe ratio. Currently defaulting to 3m of historical data + Parameters + ---------- + list_of_stocks: List[str] + List of the stocks to be included in the weights + + Returns + ------- + weights: dict + Dictionary of weights where keys are the tickers. + """ + + df = yf.download(list_of_stocks, period="3mo", group_by="ticker") + df1 = pd.DataFrame(index=df.index) + # process df + for stock in list_of_stocks: + df1[stock] = df[stock]["Adj Close"] + + mu = expected_returns.mean_historical_return(df1) + S = risk_models.sample_cov(df1) + ef = EfficientFrontier(mu, S) + weights = ef.max_sharpe() + ef.portfolio_performance(verbose=True) + return weights diff --git a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py index ef7c2d1a128..6c7db144f96 100644 --- a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py +++ b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py @@ -15,27 +15,30 @@ from gamestonk_terminal.screener import screener_controller from prompt_toolkit.completion import NestedCompleter + class PortfolioOptimization: - CHOICES = ["help", - "q", - "quit", - "ca", - "scr", - "select", - "add", - "equal_weight", - "mkt_cap"] + CHOICES = [ + "help", + "q", + "quit", + "ca", + "scr", + "select", + "add", + "equal_weight", + "mkt_cap", + "div_yield", + "max_sharpe", + ] - def __init__(self, - tickers:Set[str] = None - ): + def __init__(self, tickers: Set[str] = None): """ Construct Portfolio Optimization """ self.po_parser = argparse.ArgumentParser(add_help=False, prog="po") - self.po_parser.add_argument("cmd", choices=self.CHOICES ) + self.po_parser.add_argument("cmd", choices=self.CHOICES) self.tickers = set(tickers) # These will allow the ca menu to be re-access self.ca_ticker = None @@ -46,15 +49,11 @@ class PortfolioOptimization: """Print help""" print("\nPortfolio Optimization:") print(" help show this menu again") - print( - " q quit this menu, and shows back to main menu" - ) + print(" q quit this menu, and shows back to main menu") print(" quit quit to abandon program") print(" > ca comparison analysis menu") print(" > scr screener menu") - print( - f"\nCurrent Tickers: {('None', ', '.join(tickers))[bool(tickers)]}" - ) + print(f"\nCurrent Tickers: {('None', ', '.join(tickers))[bool(tickers)]}") print("") print(" add add ticker to optimize") print(" select overwrite current tickers with new tickers") @@ -64,9 +63,9 @@ class PortfolioOptimization: print(" Property weighted:") print(" equal_weight equally weighted portfolio") print(" mkt_cap marketcap weighted portfolio") - print(" Mean Variance Optimization") - print(" max_sharpe portfolio with maximum sharpe ratio") - + print(" div_yield dividend weighted portfolio\n") + print(" Mean Variance Optimization :") + print(" max_sharpe portfolio with maximum sharpe ratio\n") def switch(self, an_input: str): """Process and dispatch input @@ -96,31 +95,43 @@ class PortfolioOptimization: """Process Quit command - quit the program""" return True - def call_ca(self, other_args:List[str]): + def call_ca(self, other_args: List[str]): return ca_controller.menu(pd.DataFrame(), self.ca_ticker, "", "1440min") def call_scr(self, _): return screener_controller.menu() - def call_add(self, other_args:List[str]): + def call_add(self, other_args: List[str]): self.add_stocks(self, other_args) - def call_equal_weight(self, other_args:List[str]): + def call_select(self, other_args: List[str]): + self.tickers = set([]) + 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:") print(weights) print("") - def call_mkt_cap(self,other_args:List[str]): - weights = po_api.market_cap_weighting(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:") print(weights) print("") - def call_select(self,other_args:List[str]): - self.tickers = set([]) - self.add_stocks(self, 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:") + print(weights) + print("") + + def call_max_sharpe(self, other_args: List[str]): + weights = po_api.max_sharpe(self.tickers, other_args) + print("Maximum Sharpe Weights:") + print(weights) + print("") @staticmethod def add_stocks(self, other_args: List[str]): @@ -152,7 +163,6 @@ class PortfolioOptimization: for ticker in ns_parser.add_tickers: self.tickers.add(ticker) - print( f"\nCurrent Tickers: {('None', ', '.join(self.tickers))[bool(self.tickers)]}" ) @@ -167,12 +177,12 @@ class PortfolioOptimization: return cls(set([ticker] + similar)) -def menu_from_ca(ticker:str, similar:List[str]): +def menu_from_ca(ticker: str, similar: List[str]): """Portfolio Optimization Menu from ca menu that allows for jumping between""" - po_controller = PortfolioOptimization.from_ca_menu(ticker,similar) + po_controller = PortfolioOptimization.from_ca_menu(ticker, similar) po_controller.ca_ticker = ticker po_controller.ca_similar = similar - po_controller.call_help([ticker]+ similar) + po_controller.call_help([ticker] + similar) while True: # Get input command from user @@ -198,9 +208,9 @@ def menu_from_ca(ticker:str, similar:List[str]): continue -def menu(tickers:List[str]): +def menu(tickers: List[str]): """Portfolio Optimization Menu""" - if tickers == ['']: + if tickers == [""]: tickers = [] po_controller = PortfolioOptimization(tickers) po_controller.call_help(tickers) @@ -227,4 +237,3 @@ def menu(tickers:List[str]): except SystemExit: print("The command selected doesn't exist\n") continue - From 9045706a3465bed5dbcd5417830e645dee99befa Mon Sep 17 00:00:00 2001 From: Jamie Date: Mon, 12 Apr 2021 13:44:04 -0400 Subject: [PATCH 07/31] added a plot of ef and a couple other optimization techniques. + black --- .../portfolio_optimization/port_opt_api.py | 187 ++++++++++++++++-- .../portfolio_optimization/port_opt_helper.py | 51 +++++ .../port_optimization_controller.py | 45 ++++- terminal.py | 6 +- 4 files changed, 269 insertions(+), 20 deletions(-) create mode 100644 gamestonk_terminal/portfolio_optimization/port_opt_helper.py diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_api.py b/gamestonk_terminal/portfolio_optimization/port_opt_api.py index 71e697b73cc..ccf8e3731df 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_api.py +++ b/gamestonk_terminal/portfolio_optimization/port_opt_api.py @@ -3,11 +3,31 @@ __docformat__ = "numpy" import argparse from typing import List +import matplotlib.pyplot as plt +import numpy as np import pandas as pd import yfinance as yf -from pypfopt.efficient_frontier import EfficientFrontier +from pypfopt import plotting from pypfopt import risk_models from pypfopt import expected_returns +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.config_plot import PLOT_DPI +from gamestonk_terminal import feature_flags as gtff + +period_choices = [ + "1d", + "5d", + "1mo", + "3mo", + "6mo", + "1y", + "2y", + "5y", + "10y", + "ytd", + "max", +] def equal_weight(list_of_stocks: List[str], other_args: List[str]): @@ -65,29 +85,166 @@ def property_weighting( return weights -def max_sharpe(list_of_stocks: List[str], other_args: List[str]): +def show_ef(list_of_stocks: List[str], other_args: List[str]): + 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( + "-n", default=300, dest="n_port", help="number of portfolios to simulate" + ) + 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) + mu = expected_returns.mean_historical_return(stock_prices) + S = risk_models.sample_cov(stock_prices) + ef = EfficientFrontier(mu, S) + fig, ax = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI) + + # Generate random portfolios + n_samples = ns_parser.n_port + w = np.random.dirichlet(np.ones(len(mu)), n_samples) + rets = w.dot(mu) + stds = np.sqrt(np.diag(w @ S @ w.T)) + sharpes = rets / stds + ax.scatter(stds, rets, marker=".", c=sharpes, cmap="viridis_r") + + plotting.plot_efficient_frontier(ef, ax=ax, show_assets=True) + # Find the tangency portfolio + ef.max_sharpe() + ret_sharpe, std_sharpe, _ = ef.portfolio_performance() + ax.scatter(std_sharpe, ret_sharpe, marker="*", s=100, c="r", label="Max Sharpe") + + ax.set_title("Efficient Frontier") + ax.legend() + plt.tight_layout() + plt.grid(b=True, which="major", color="#666666", linestyle="-") + plt.minorticks_on() + plt.grid(b=True, which="minor", color="#999999", linestyle="-", alpha=0.2) + + if gtff.USE_ION: + plt.ion() + + plt.show() + print("") + + 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 that maximizes the sharpe ratio. Currently defaulting to 3m of historical data + Return a portfolio based on condition in port_type Currently defaulting 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. """ - df = yf.download(list_of_stocks, period="3mo", group_by="ticker") - df1 = pd.DataFrame(index=df.index) - # process df - for stock in list_of_stocks: - df1[stock] = df[stock]["Adj Close"] + parser = argparse.ArgumentParser(add_help=False, prog="ef") - mu = expected_returns.mean_historical_return(df1) - S = risk_models.sample_cov(df1) - ef = EfficientFrontier(mu, S) - weights = ef.max_sharpe() - ef.portfolio_performance(verbose=True) - return weights + parser.add_argument( + "-p", + "--period", + default="3mo", + dest="period", + help="period to get yfinance data from", + choices=period_choices, + ) + + 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: round(value, 5) for key, value in ef_sharpe.items()} + print("") + ef.portfolio_performance(verbose=True) + print("") + return weights + except Exception as e: + print(e) + print("") + 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: round(value, 5) for key, value in ef_min_vol.items()} + 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 + + 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: round(value, 5) for key, value in ef_eff_risk.items()} + 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: round(value, 5) for key, value in ef_eff_risk.items()} + print("") + ef.portfolio_performance(verbose=True) + print("") + return weights + except Exception as e: + print(e) + print("") + return {} + + else: + raise ValueError("EF Method not found") + return {} diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_helper.py b/gamestonk_terminal/portfolio_optimization/port_opt_helper.py new file mode 100644 index 00000000000..c5107aa572c --- /dev/null +++ b/gamestonk_terminal/portfolio_optimization/port_opt_helper.py @@ -0,0 +1,51 @@ +""" Portfolio Optimization Helper Functions""" +__docformat__ = "numpy" + +from typing import List +import pandas as pd +import yfinance as yf +from pypfopt.efficient_frontier import EfficientFrontier +from pypfopt import risk_models +from pypfopt import expected_returns + + +def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFrame: + """ + + Parameters + ---------- + list_of_stocks: List[str] + List of tickers to get historical data for + period: str + Period to get data from yfinance + + Returns + ------- + 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_closes = pd.DataFrame(index=stock_prices.index) + # process df + for stock in list_of_stocks: + stock_closes[stock] = stock_prices[stock]["Adj Close"] + return stock_closes + + +def prepare_efficient_frontier(stock_prices: pd.DataFrame): + """ + Take in a dataframe of prices and return an efficient frontier object + Parameters + ---------- + stock_prices : DataFrame + DataFrame where indices are DateTime and columns are stocks + + Returns + ------- + ef: EfficientFrontier + EfficientFrontier object + """ + mu = expected_returns.mean_historical_return(stock_prices) + S = risk_models.sample_cov(stock_prices) + ef = EfficientFrontier(mu, S) + return ef diff --git a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py index 6c7db144f96..1adb748df92 100644 --- a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py +++ b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py @@ -4,7 +4,7 @@ __docformat__ = "numpy" import argparse from typing import List, Set -from datetime import datetime +import matplotlib.pyplot as plt import pandas as pd from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import get_flair, parse_known_args_and_warn @@ -30,6 +30,11 @@ class PortfolioOptimization: "mkt_cap", "div_yield", "max_sharpe", + "min_vol", + "eff_risk", + "eff_ret", + "show_ef", + "yolo", ] def __init__(self, tickers: Set[str] = None): @@ -65,7 +70,13 @@ class PortfolioOptimization: print(" mkt_cap marketcap weighted portfolio") print(" div_yield dividend weighted portfolio\n") print(" Mean Variance Optimization :") - print(" max_sharpe portfolio with maximum sharpe ratio\n") + 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 @@ -128,11 +139,39 @@ class PortfolioOptimization: print("") def call_max_sharpe(self, other_args: List[str]): - weights = po_api.max_sharpe(self.tickers, other_args) + weights = po_api.ef_portfolio(self.tickers, "max_sharpe", other_args) print("Maximum Sharpe Weights:") print(weights) print("") + def call_min_vol(self, other_args: List[str]): + weights = po_api.ef_portfolio(self.tickers, "min_volatility", other_args) + print("Minimum volatility Weights:") + print(weights) + print("") + + 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") + print(weights) + print("") + + 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") + print(weights) + print("") + + def call_show_ef(self, other_args): + po_api.show_ef(self.tickers, other_args) + print("") + + def call_yolo(self, _): + # Easter egg :) + print("DFV YOLO") + print({"GME": 200}) + print("") + @staticmethod def add_stocks(self, other_args: List[str]): diff --git a/terminal.py b/terminal.py index 123c73a78da..b47ba357026 100644 --- a/terminal.py +++ b/terminal.py @@ -31,7 +31,9 @@ from gamestonk_terminal.residuals_analysis import ra_controller from gamestonk_terminal.portfolio import port_controller from gamestonk_terminal.cryptocurrency import crypto_controller from gamestonk_terminal.screener import screener_controller -from gamestonk_terminal.portfolio_optimization import port_optimization_controller as po_controller +from gamestonk_terminal.portfolio_optimization import ( + port_optimization_controller as po_controller, +) # import warnings # warnings.simplefilter("always") @@ -89,7 +91,7 @@ def main(): "pa", "crypto", "ra", - "po" + "po", ] menu_parser.add_argument("opt", choices=choices) From b5485f8f5e7448f2d835b7f5f63e2747fcb35495 Mon Sep 17 00:00:00 2001 From: Jamie Date: Mon, 12 Apr 2021 14:50:49 -0400 Subject: [PATCH 08/31] Updated ReadME's and roadmap --- ROADMAP.md | 7 +- gamestonk_terminal/README.md | 15 +++ gamestonk_terminal/main_helper.py | 4 +- .../portfolio_optimization/README.md | 122 +++++++++++++++++- 4 files changed, 140 insertions(+), 8 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 1642af3fe10..f5c466f3a9a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -142,8 +142,13 @@ * [ ] Add Brokers (td, webull, etc) (@jmaslek) * [ ] Refactoring * [ ] Summaries / tear sheets (@jmaslek) -* [ ] Portfolio Optimization (@jmaslek) +___ +##Portfolio Optimization +* [x] Basic Optimization through PyPortFolioOpt(@jmaslek) - [PR #329](https://github.com/DidierRLopes/GamestonkTerminal/pull/329) +**NEXT** +* [ ] Decide what features should be present +* [ ] Allow for more custom optimization constrains --- ## FRED diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 4ab6d9c5790..2b6d79c7b2e 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -241,6 +241,21 @@ Command|Description|Brokers `hold` | view net holdings across all logins   +## Portfolio Optimization [»](portfolio_optimization/README.md) +command|desciription +------|------ +`equal_weight`|Returns equal weights for all selected stocks +`mkt_cap`| Returns weights that are weighted by market cap +`div_yield`|Returns weights that are weighted by dividend yield +`max_sharpe`| Returns portfolio of Efficient Frontier with maximum sharpe ratio +`min_vol`| Returns portfolio on Efficient Frontier with minimum volatility +`eff_risk`| Returns portfolio on Efficient Frontier that maximizes returns at a given risk +`eff_ret`| Returns portfolio on Efficient Frontier that minimizes risk at a given return level +`show_eff`|Plots random portfolios and shows the Efficient Frontier + +  + + ## Cryptocurrency [»](cryptocurrency/README.md) Command|Description diff --git a/gamestonk_terminal/main_helper.py b/gamestonk_terminal/main_helper.py index e09a7d2023c..40c8f6714ba 100644 --- a/gamestonk_terminal/main_helper.py +++ b/gamestonk_terminal/main_helper.py @@ -60,7 +60,9 @@ def print_help(s_ticker, s_start, s_interval, b_is_market_open): ) print(" pa portfolio analysis, \t\t supports: robinhood, alpaca, ally ") print(" crypto cryptocurrencies, \t\t uses coingecko api") - print(" po portfolio optimization, \t\t TBA") + print( + " po portfolio optimization, \t\t optimal portfolio weights from pyportfolioopt" + ) if s_ticker: print( diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md index b546337b1be..84dbd94d42e 100644 --- a/gamestonk_terminal/portfolio_optimization/README.md +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -1,16 +1,126 @@ # Portfolio Optimization -This menu aims to discover optimal portfolios for selected stocks +This menu aims to discover optimal portfolios for selected stocks. +Currently we support two methods: +* [Property Weighting](#weighting) + * Equal Weights + * Market Cap Weighting + * Divident Yield Weighting +* [Mean-Variance-Optimization](#eff_front) + * Max sharpe ratio + * Minimum volatility + * Maximum returns at given risk level + * Minimum risk level at a given return + +## Procedure +There are three ways to load stocks to be analyzed. +* add +* select +* from ca menu ###add -Adds tickers to current list for optimization +Adds selected tickers to the menu to be considered + ```` -usage: add ticker1,ticker2,... +usage: add ticker1,ticker2,ticker3,... ```` -###optimize -Runs optimization +###select +Clears current list and loads selected ticker ```` -usage: optimize +usage: select ticker1,ticker2,ticker3,... ```` +### ca menu +From the ca menu, the loaded ticker and the selected similar tickers can be loaded by entering the `> po` menu. + +From this `po` menu, you can return to `ca` using `ca`, which will keep your loaded stock, but reset the selected stocks from that menu. + +Once your stocks are listed, you can select one of the options. +## Property weighted +* equal weights +* market cap weighted +* dividend yield weighted +### equal weights +```` +equal_weight +```` +Returns `{"Ticker" : 1/# of tickers}` +### market cap weighted +```` +mkt_cap +```` +Returns a dictionary where each weight is given as (Company Market Cap)/(Sum all included market caps) +###dividened yield weighted +```` +div_yield +```` +Returns weights based on relative divident yield. + +## Mean Variance Optimization + +These approaches are based off of the efficient frontier approach, which is meant to solve the following optimization problem. + + + +With constraints: + + + +\ + + +Where S is the covariance matrix between stocks and R is the expected returns. The condition that all weights add up to 1 +just implies that you want to have a net long portfolio (with no margin). +A long-short portfolio can have negative weights and usually wants to have everything add up to 0 for a market-neutral strategy. + +Currently, we do not allow for changing risk models or adding constraints. If there is something spefic, please submit a feature request, or if you can +write it, feel free to add a PR! + +All of our current implementations use the [PyPortFolioOpt](#https://pyportfolioopt.readthedocs.io/en/latest/index.html) package. + +### max_sharpe +The sharpe ratio is defined as + +(Mean Returns - Risk Free Rate)/(Standard Deviation of Returns) + +The usage is: +```` +max_sharpe [-p PERIOD] +```` +* -p/--period Amount of time to retreive data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defualts to 3mo. + +### min_vol +This portfolio minimizes the total volatility, which also means it has the smallest returns among the efficient frontier. +The usage is: +```` +min_vol [-p PERIOD] +```` +* -p/--period Amount of time to retreive data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defualts to 3mo. + +### eff_risk +This portfolio maximizes the returns at a given risk tolerance +The usage is: +```` +eff_risk [-p PERIOD] [-r --risk RISK_LEVEL] +```` +* -p/--period Amount of time to retreive data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defualts to 3mo. +* -r/--risk Risk tolerance. 5% is 0.05. + +### eff_ret +This portfolio minimizes the risk at a given return level +The usage is: +```` +eff_ret [-p PERIOD] [-r --return] +```` +* -p/--period Amount of time to retreive data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defualts to 3mo. +* -r/--return. Desired return. 5% is 0.05. + +### show_eff +This function plots random portfolios basd 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 retreive data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defualts to 3mo. +* -n Number of portfolios to simulate. \ No newline at end of file From 60604c26364009b317aa4c78f58ef16843dbf0df Mon Sep 17 00:00:00 2001 From: Jamie Date: Mon, 12 Apr 2021 15:01:15 -0400 Subject: [PATCH 09/31] black + pylint --- .../portfolio_optimization/port_opt_api.py | 20 +++++++++++-------- .../port_optimization_controller.py | 5 ++--- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_api.py b/gamestonk_terminal/portfolio_optimization/port_opt_api.py index ccf8e3731df..059affe85eb 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_api.py +++ b/gamestonk_terminal/portfolio_optimization/port_opt_api.py @@ -5,13 +5,16 @@ import argparse from typing import List import matplotlib.pyplot as plt import numpy as np -import pandas as pd import yfinance as yf from pypfopt import plotting 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.port_opt_helper import ( + process_stocks, + prepare_efficient_frontier, +) from gamestonk_terminal.config_plot import PLOT_DPI from gamestonk_terminal import feature_flags as gtff @@ -30,7 +33,7 @@ period_choices = [ ] -def equal_weight(list_of_stocks: List[str], other_args: List[str]): +def equal_weight(list_of_stocks: List[str], _): """ Equally weighted portfolio, where weight = 1/# of stocks @@ -53,9 +56,7 @@ def equal_weight(list_of_stocks: List[str], other_args: List[str]): return weights -def property_weighting( - list_of_stocks: List[str], property_type: str, other_args: List[str] -): +def property_weighting(list_of_stocks: List[str], property_type: str, _): """ Property weighted portfolio where each weight is the relative fraction. Examples Parameters @@ -76,7 +77,7 @@ def property_weighting( prop_sum = 0 for stock in list_of_stocks: - stock_prop = yf.Ticker(stock).info["marketCap"] + stock_prop = yf.Ticker(stock).info[property_type] prop[stock] = stock_prop prop_sum += stock_prop for k, v in prop.items(): @@ -184,6 +185,8 @@ def ef_portfolio(list_of_stocks: List[str], port_type: str, other_args: List[str except Exception as e: print(e) print("") + return {} + elif port_type == "min_volatility": try: ns_parser = parse_known_args_and_warn(parser, other_args) @@ -198,6 +201,7 @@ def ef_portfolio(list_of_stocks: List[str], port_type: str, other_args: List[str ef.portfolio_performance(verbose=True) print("") return weights + except Exception as e: print(e) print("") @@ -223,6 +227,7 @@ def ef_portfolio(list_of_stocks: List[str], port_type: str, other_args: List[str print(e) print("") return {} + elif port_type == "eff_ret": parser.add_argument( @@ -247,4 +252,3 @@ def ef_portfolio(list_of_stocks: List[str], port_type: str, other_args: List[str else: raise ValueError("EF Method not found") - return {} diff --git a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py index 1adb748df92..70b6f8d69e2 100644 --- a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py +++ b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py @@ -6,6 +6,7 @@ import argparse from typing import List, Set import matplotlib.pyplot as plt import pandas as pd +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 @@ -13,8 +14,6 @@ from gamestonk_terminal.portfolio_optimization import port_opt_api as po_api from gamestonk_terminal.comparison_analysis import ca_controller from gamestonk_terminal.screener import screener_controller -from prompt_toolkit.completion import NestedCompleter - class PortfolioOptimization: @@ -106,7 +105,7 @@ class PortfolioOptimization: """Process Quit command - quit the program""" return True - def call_ca(self, other_args: List[str]): + def call_ca(self, _): return ca_controller.menu(pd.DataFrame(), self.ca_ticker, "", "1440min") From 93a7216bf098edfbff94c7bea2ae077b65c76345 Mon Sep 17 00:00:00 2001 From: Jamie Date: Mon, 12 Apr 2021 15:04:56 -0400 Subject: [PATCH 10/31] spelling --- gamestonk_terminal/portfolio_optimization/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md index 84dbd94d42e..6d3e96b5a09 100644 --- a/gamestonk_terminal/portfolio_optimization/README.md +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -74,7 +74,7 @@ Where S is the covariance matrix between stocks and R is the expected returns. just implies that you want to have a net long portfolio (with no margin). A long-short portfolio can have negative weights and usually wants to have everything add up to 0 for a market-neutral strategy. -Currently, we do not allow for changing risk models or adding constraints. If there is something spefic, please submit a feature request, or if you can +Currently, we do not allow for changing risk models or adding constraints. If there is something specific, please submit a feature request, or if you can write it, feel free to add a PR! All of our current implementations use the [PyPortFolioOpt](#https://pyportfolioopt.readthedocs.io/en/latest/index.html) package. @@ -88,7 +88,7 @@ The usage is: ```` max_sharpe [-p PERIOD] ```` -* -p/--period Amount of time to retreive data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defualts to 3mo. +* -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. ### min_vol This portfolio minimizes the total volatility, which also means it has the smallest returns among the efficient frontier. @@ -96,7 +96,7 @@ The usage is: ```` min_vol [-p PERIOD] ```` -* -p/--period Amount of time to retreive data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defualts to 3mo. +* -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. ### eff_risk This portfolio maximizes the returns at a given risk tolerance @@ -104,7 +104,7 @@ The usage is: ```` eff_risk [-p PERIOD] [-r --risk RISK_LEVEL] ```` -* -p/--period Amount of time to retreive data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defualts to 3mo. +* -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. ### eff_ret @@ -113,7 +113,7 @@ The usage is: ```` eff_ret [-p PERIOD] [-r --return] ```` -* -p/--period Amount of time to retreive data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defualts to 3mo. +* -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. ### show_eff @@ -122,5 +122,5 @@ The usage is: ```` show_eff [-p PERIOD] [-n N_PORTFOLIOS] ```` -* -p/--period Amount of time to retreive data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defualts to 3mo. +* -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 From ffdb7098127313f6348e4e46ac4af38b166c92cb Mon Sep 17 00:00:00 2001 From: Jamie Date: Mon, 12 Apr 2021 15:39:56 -0400 Subject: [PATCH 11/31] mypy? --- .../portfolio_optimization/port_optimization_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py index 70b6f8d69e2..e7cc26b5bc1 100644 --- a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py +++ b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py @@ -36,7 +36,7 @@ class PortfolioOptimization: "yolo", ] - def __init__(self, tickers: Set[str] = None): + def __init__(self, tickers: List[str] = None): """ Construct Portfolio Optimization """ @@ -49,7 +49,7 @@ class PortfolioOptimization: self.ca_similar = None @staticmethod - def print_help(tickers: Set[str]): + def print_help(tickers: List[str]): """Print help""" print("\nPortfolio Optimization:") print(" help show this menu again") From 52dab41d7eb088522f164eb0f5d69a8da42fb26a Mon Sep 17 00:00:00 2001 From: Jamie Date: Mon, 12 Apr 2021 15:42:16 -0400 Subject: [PATCH 12/31] flake --- .../portfolio_optimization/port_optimization_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py index e7cc26b5bc1..1026c22d5a3 100644 --- a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py +++ b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py @@ -3,7 +3,7 @@ __docformat__ = "numpy" import argparse -from typing import List, Set +from typing import List import matplotlib.pyplot as plt import pandas as pd from prompt_toolkit.completion import NestedCompleter From dcf87e30170a815d763988b4e567330445a88e32 Mon Sep 17 00:00:00 2001 From: Jamie Date: Mon, 12 Apr 2021 17:08:15 -0400 Subject: [PATCH 13/31] get rid of po -> ca/scr --- .../comparison_analysis/ca_controller.py | 2 +- .../port_optimization_controller.py | 65 +++---------------- 2 files changed, 9 insertions(+), 58 deletions(-) diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py index 7db095a4038..4d8b7f33670 100644 --- a/gamestonk_terminal/comparison_analysis/ca_controller.py +++ b/gamestonk_terminal/comparison_analysis/ca_controller.py @@ -320,7 +320,7 @@ class ComparisonAnalysisController: def call_po(self, _): """Open Portfolio Optimization menu with ticker and similar""" - return po_controller.menu_from_ca(self.ticker, self.similar) + return po_controller.menu([self.ticker] + self.similar) def menu(stock: pd.DataFrame, ticker: str, start: datetime, interval: str): diff --git a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py index 1026c22d5a3..18e04383afd 100644 --- a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py +++ b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py @@ -21,8 +21,6 @@ class PortfolioOptimization: "help", "q", "quit", - "ca", - "scr", "select", "add", "equal_weight", @@ -55,8 +53,6 @@ class PortfolioOptimization: print(" help show this menu again") print(" q quit this menu, and shows back to main menu") print(" quit quit to abandon program") - print(" > ca comparison analysis menu") - print(" > scr screener menu") print(f"\nCurrent Tickers: {('None', ', '.join(tickers))[bool(tickers)]}") print("") print(" add add ticker to optimize") @@ -64,16 +60,14 @@ class PortfolioOptimization: print("") print("Optimization:") print("") - print(" Property weighted:") - print(" equal_weight equally weighted portfolio") - print(" mkt_cap marketcap weighted portfolio") - print(" div_yield dividend weighted portfolio\n") - print(" Mean Variance Optimization :") - 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(" 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") @@ -105,13 +99,6 @@ class PortfolioOptimization: """Process Quit command - quit the program""" return True - def call_ca(self, _): - - return ca_controller.menu(pd.DataFrame(), self.ca_ticker, "", "1440min") - - def call_scr(self, _): - return screener_controller.menu() - def call_add(self, other_args: List[str]): self.add_stocks(self, other_args) @@ -173,7 +160,6 @@ class PortfolioOptimization: @staticmethod def add_stocks(self, other_args: List[str]): - """ Add ticker to current list for optimization""" parser = argparse.ArgumentParser( add_help=False, @@ -210,41 +196,6 @@ class PortfolioOptimization: print("") - @classmethod - def from_ca_menu(cls, ticker: str, similar: List[str]): - return cls(set([ticker] + similar)) - - -def menu_from_ca(ticker: str, similar: List[str]): - """Portfolio Optimization Menu from ca menu that allows for jumping between""" - po_controller = PortfolioOptimization.from_ca_menu(ticker, similar) - po_controller.ca_ticker = ticker - po_controller.ca_similar = similar - po_controller.call_help([ticker] + similar) - - while True: - # Get input command from user - if session and gtff.USE_PROMPT_TOOLKIT: - completer = NestedCompleter.from_nested_dict( - {c: None for c in po_controller.CHOICES} - ) - an_input = session.prompt( - f"{get_flair()} (po)> ", - completer=completer, - ) - else: - an_input = input(f"{get_flair()} (po)> ") - - try: - process_input = po_controller.switch(an_input) - - if process_input is not None: - return process_input - - except SystemExit: - print("The command selected doesn't exist\n") - continue - def menu(tickers: List[str]): """Portfolio Optimization Menu""" From 1e59b48557d2e7341fe7b956212693ffa339f3cf Mon Sep 17 00:00:00 2001 From: Jamie Date: Mon, 12 Apr 2021 17:20:07 -0400 Subject: [PATCH 14/31] rename_po/ update dependencies --- .../comparison_analysis/ca_controller.py | 2 +- ...ization_controller.py => po_controller.py} | 6 ++-- .../portfolio_optimization/port_opt_api.py | 36 +++++++++++++++---- terminal.py | 4 +-- 4 files changed, 35 insertions(+), 13 deletions(-) rename gamestonk_terminal/portfolio_optimization/{port_optimization_controller.py => po_controller.py} (98%) diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py index 4d8b7f33670..bbdc02f9bd6 100644 --- a/gamestonk_terminal/comparison_analysis/ca_controller.py +++ b/gamestonk_terminal/comparison_analysis/ca_controller.py @@ -16,7 +16,7 @@ from gamestonk_terminal.comparison_analysis import market_watch_api as mw_api from gamestonk_terminal.comparison_analysis import finbrain_api as f_api from gamestonk_terminal.comparison_analysis import finviz_compare_view from gamestonk_terminal.portfolio_optimization import ( - port_optimization_controller as po_controller, + po_controller as po_controller, ) from gamestonk_terminal.menu import session from prompt_toolkit.completion import NestedCompleter diff --git a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py similarity index 98% rename from gamestonk_terminal/portfolio_optimization/port_optimization_controller.py rename to gamestonk_terminal/portfolio_optimization/po_controller.py index 18e04383afd..a5d27134ba6 100644 --- a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -34,14 +34,14 @@ class PortfolioOptimization: "yolo", ] - def __init__(self, tickers: List[str] = None): + def __init__(self, tickers: List[str] = []): """ Construct Portfolio Optimization """ self.po_parser = argparse.ArgumentParser(add_help=False, prog="po") self.po_parser.add_argument("cmd", choices=self.CHOICES) - self.tickers = set(tickers) + self.tickers = list(set(tickers)) # These will allow the ca menu to be re-access self.ca_ticker = None self.ca_similar = None @@ -103,7 +103,7 @@ class PortfolioOptimization: self.add_stocks(self, other_args) def call_select(self, other_args: List[str]): - self.tickers = set([]) + self.tickers = [] self.add_stocks(self, other_args) def call_equal_weight(self, other_args: List[str]): diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_api.py b/gamestonk_terminal/portfolio_optimization/port_opt_api.py index 059affe85eb..1653ebdf151 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_api.py +++ b/gamestonk_terminal/portfolio_optimization/port_opt_api.py @@ -33,7 +33,7 @@ period_choices = [ ] -def equal_weight(list_of_stocks: List[str], _): +def equal_weight(list_of_stocks: List[str], other_args: List[str]): """ Equally weighted portfolio, where weight = 1/# of stocks @@ -48,12 +48,36 @@ def equal_weight(list_of_stocks: List[str], _): Dictionary of weights where keys are the tickers """ - weights = {} - n_stocks = len(list_of_stocks) - for stock in list_of_stocks: - weights[stock] = round(1 / n_stocks, 5) + parser = argparse.ArgumentParser( + add_help=False, + prog="equal_weight", + description="Return equally weighted portfolio holdings", + ) - return weights + parser.add_argument( + "-v", + "--value", + default=1, + type=float, + dest="value", + help="Portfolio amount to determine amount spent on each", + ) + try: + 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) + values[stock] = ns_parser.value * round(1 / n_stocks, 5) + + return values + + except Exception as e: + print(e) + print("") def property_weighting(list_of_stocks: List[str], property_type: str, _): diff --git a/terminal.py b/terminal.py index b47ba357026..6b19c501151 100644 --- a/terminal.py +++ b/terminal.py @@ -31,9 +31,7 @@ from gamestonk_terminal.residuals_analysis import ra_controller from gamestonk_terminal.portfolio import port_controller from gamestonk_terminal.cryptocurrency import crypto_controller from gamestonk_terminal.screener import screener_controller -from gamestonk_terminal.portfolio_optimization import ( - port_optimization_controller as po_controller, -) +from gamestonk_terminal.portfolio_optimization import po_controller # import warnings # warnings.simplefilter("always") From 1fbd0152bde2c76d73a08b5f8180e7ac66a94c88 Mon Sep 17 00:00:00 2001 From: Jamie Date: Mon, 12 Apr 2021 17:20:28 -0400 Subject: [PATCH 15/31] rename_po/ update dependencies --- gamestonk_terminal/comparison_analysis/ca_controller.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py index bbdc02f9bd6..b06ed049c46 100644 --- a/gamestonk_terminal/comparison_analysis/ca_controller.py +++ b/gamestonk_terminal/comparison_analysis/ca_controller.py @@ -15,9 +15,7 @@ from gamestonk_terminal.comparison_analysis import yahoo_finance_api as yf_api from gamestonk_terminal.comparison_analysis import market_watch_api as mw_api from gamestonk_terminal.comparison_analysis import finbrain_api as f_api from gamestonk_terminal.comparison_analysis import finviz_compare_view -from gamestonk_terminal.portfolio_optimization import ( - po_controller as po_controller, -) +from gamestonk_terminal.portfolio_optimization import po_controller from gamestonk_terminal.menu import session from prompt_toolkit.completion import NestedCompleter From 6dd25ef6081c68bb0b45848a017b6f5179b87213 Mon Sep 17 00:00:00 2001 From: Jamie Date: Mon, 12 Apr 2021 17:34:16 -0400 Subject: [PATCH 16/31] updated some functions to be argparsed --- .../portfolio_optimization/po_controller.py | 20 +++++----- .../portfolio_optimization/port_opt_api.py | 39 +++++++++++++++---- .../portfolio_optimization/port_opt_helper.py | 13 +++++++ 3 files changed, 55 insertions(+), 17 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index a5d27134ba6..3bd0442e1ef 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -11,6 +11,7 @@ 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.comparison_analysis import ca_controller from gamestonk_terminal.screener import screener_controller @@ -109,43 +110,43 @@ class PortfolioOptimization: def call_equal_weight(self, other_args: List[str]): weights = po_api.equal_weight(self.tickers, other_args) print("Optimal Weights for Equal Weighting:") - print(weights) + display_weights(weights) print("") def call_mkt_cap(self, other_args: List[str]): weights = po_api.property_weighting(self.tickers, "marketCap", other_args) print("Market Cap Weighting Weights:") - print(weights) + display_weights(weights) print("") def call_div_yield(self, other_args: List[str]): weights = po_api.property_weighting(self.tickers, "dividendYield", other_args) print("Dividend Weighed Weights:") - print(weights) + display_weights(weights) print("") def call_max_sharpe(self, other_args: List[str]): weights = po_api.ef_portfolio(self.tickers, "max_sharpe", other_args) print("Maximum Sharpe Weights:") - print(weights) + display_weights(weights) print("") def call_min_vol(self, other_args: List[str]): weights = po_api.ef_portfolio(self.tickers, "min_volatility", other_args) print("Minimum volatility Weights:") - print(weights) + display_weights(weights) print("") 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") - print(weights) + display_weights(weights) print("") 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") - print(weights) + display_weights(weights) print("") def call_show_ef(self, other_args): @@ -183,9 +184,10 @@ class PortfolioOptimization: ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return - + tickers = set(self.tickers) for ticker in ns_parser.add_tickers: - self.tickers.add(ticker) + tickers.add(ticker) + self.tickers = list(tickers) print( f"\nCurrent Tickers: {('None', ', '.join(self.tickers))[bool(self.tickers)]}" diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_api.py b/gamestonk_terminal/portfolio_optimization/port_opt_api.py index 1653ebdf151..09771095b6b 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_api.py +++ b/gamestonk_terminal/portfolio_optimization/port_opt_api.py @@ -80,7 +80,7 @@ def equal_weight(list_of_stocks: List[str], other_args: List[str]): print("") -def property_weighting(list_of_stocks: List[str], property_type: str, _): +def property_weighting(list_of_stocks: List[str], property_type: str, other_args:List[str]): """ Property weighted portfolio where each weight is the relative fraction. Examples Parameters @@ -96,18 +96,41 @@ def property_weighting(list_of_stocks: List[str], property_type: str, _): weights: dict Dictionary of weights where keys are the tickers """ + parser = argparse.ArgumentParser( + add_help=False, + prog="market_cap_weighted", + description="Return portfolio weights/values that are weighted by marketcap", + ) + + parser.add_argument( + "-v", + "--value", + default=1, + type=float, + dest="value", + help="Portfolio amount to determine amount spent on each", + ) weights = {} prop = {} prop_sum = 0 - for stock in list_of_stocks: - stock_prop = yf.Ticker(stock).info[property_type] - prop[stock] = stock_prop - prop_sum += stock_prop - for k, v in prop.items(): - weights[k] = round(v / prop_sum, 5) + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + for stock in list_of_stocks: + stock_prop = yf.Ticker(stock).info[property_type] + prop[stock] = stock_prop + prop_sum += stock_prop + for k, v in prop.items(): + weights[k] = round(v / prop_sum, 5) * ns_parser.value - return weights + return weights + + except Exception as e: + print(e) + print("") + return def show_ef(list_of_stocks: List[str], other_args: List[str]): diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_helper.py b/gamestonk_terminal/portfolio_optimization/port_opt_helper.py index c5107aa572c..d4e54ef1769 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_helper.py +++ b/gamestonk_terminal/portfolio_optimization/port_opt_helper.py @@ -49,3 +49,16 @@ def prepare_efficient_frontier(stock_prices: pd.DataFrame): S = risk_models.sample_cov(stock_prices) ef = EfficientFrontier(mu, S) return ef + + +def display_weights(weights: dict): + """ + Print weights in a nice format + Parameters + ---------- + weights: dict + weights to display. Keys are stocks. Values are either weights or values if -v specified + """ + + weight_df = pd.DataFrame.from_dict(data=weights, orient="index", columns=["value"]) + print(weight_df) From 7087fc8a0c5fba78d8e76ac1729d6561d4b69090 Mon Sep 17 00:00:00 2001 From: Jamie Date: Mon, 12 Apr 2021 22:12:21 -0400 Subject: [PATCH 17/31] Added pie chart options! --- .../portfolio_optimization/po_controller.py | 3 -- .../portfolio_optimization/port_opt_api.py | 53 ++++++++++++++++-- .../portfolio_optimization/port_opt_helper.py | 54 ++++++++++++++++++- 3 files changed, 102 insertions(+), 8 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index 3bd0442e1ef..cda42f940cf 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -5,15 +5,12 @@ __docformat__ = "numpy" import argparse from typing import List import matplotlib.pyplot as plt -import pandas as pd 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.comparison_analysis import ca_controller -from gamestonk_terminal.screener import screener_controller class PortfolioOptimization: diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_api.py b/gamestonk_terminal/portfolio_optimization/port_opt_api.py index 09771095b6b..3a120ec8eed 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_api.py +++ b/gamestonk_terminal/portfolio_optimization/port_opt_api.py @@ -14,6 +14,7 @@ from gamestonk_terminal.helper_funcs import parse_known_args_and_warn, plot_auto from gamestonk_terminal.portfolio_optimization.port_opt_helper import ( process_stocks, prepare_efficient_frontier, + pie_chart_weights, ) from gamestonk_terminal.config_plot import PLOT_DPI from gamestonk_terminal import feature_flags as gtff @@ -62,6 +63,13 @@ def equal_weight(list_of_stocks: List[str], other_args: List[str]): dest="value", help="Portfolio amount to determine amount spent on each", ) + 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: @@ -72,7 +80,8 @@ def equal_weight(list_of_stocks: List[str], other_args: List[str]): for stock in list_of_stocks: weights[stock] = round(1 / n_stocks, 5) values[stock] = ns_parser.value * round(1 / n_stocks, 5) - + if ns_parser.pie: + pie_chart_weights(values) return values except Exception as e: @@ -80,7 +89,9 @@ def equal_weight(list_of_stocks: List[str], other_args: List[str]): print("") -def property_weighting(list_of_stocks: List[str], property_type: str, other_args:List[str]): +def property_weighting( + list_of_stocks: List[str], property_type: str, other_args: List[str] +): """ Property weighted portfolio where each weight is the relative fraction. Examples Parameters @@ -110,6 +121,13 @@ def property_weighting(list_of_stocks: List[str], property_type: str, other_args dest="value", help="Portfolio amount to determine amount spent on each", ) + parser.add_argument( + "--pie", + action="store_true", + dest="pie", + default=False, + help="Display a pie chart for weights", + ) weights = {} prop = {} prop_sum = 0 @@ -124,7 +142,8 @@ def property_weighting(list_of_stocks: List[str], property_type: str, other_args 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 except Exception as e: @@ -147,6 +166,7 @@ def show_ef(list_of_stocks: List[str], other_args: List[str]): parser.add_argument( "-n", default=300, dest="n_port", help="number of portfolios to simulate" ) + try: ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: @@ -215,6 +235,21 @@ def ef_portfolio(list_of_stocks: List[str], port_type: str, other_args: List[str choices=period_choices, ) + parser.add_argument( + "-v", + "--value", + dest="value", + help="Portfolio amount to determine amount spent on each", + 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) @@ -225,6 +260,8 @@ def ef_portfolio(list_of_stocks: List[str], port_type: str, other_args: List[str ef = prepare_efficient_frontier(stock_prices) ef_sharpe = dict(ef.max_sharpe()) weights = {key: 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("") @@ -244,6 +281,8 @@ def ef_portfolio(list_of_stocks: List[str], port_type: str, other_args: List[str ef = prepare_efficient_frontier(stock_prices) ef_min_vol = dict(ef.min_volatility()) weights = {key: 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("") @@ -261,11 +300,14 @@ def ef_portfolio(list_of_stocks: List[str], port_type: str, other_args: List[str 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: 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("") @@ -288,10 +330,13 @@ def ef_portfolio(list_of_stocks: List[str], port_type: str, other_args: List[str ef = prepare_efficient_frontier(stock_prices) ef_eff_risk = dict(ef.efficient_return(ns_parser.target_return)) weights = {key: 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("") diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_helper.py b/gamestonk_terminal/portfolio_optimization/port_opt_helper.py index d4e54ef1769..f8695de0bfa 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_helper.py +++ b/gamestonk_terminal/portfolio_optimization/port_opt_helper.py @@ -2,11 +2,16 @@ __docformat__ = "numpy" from typing import List +import math import pandas as pd +import matplotlib.pyplot as plt import yfinance as yf from pypfopt.efficient_frontier import EfficientFrontier 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 def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFrame: @@ -59,6 +64,53 @@ def display_weights(weights: dict): weights: dict weights to display. Keys are stocks. Values are either weights or values if -v specified """ - + if not weights: + return weight_df = pd.DataFrame.from_dict(data=weights, orient="index", columns=["value"]) print(weight_df) + + +def pie_chart_weights(weights: dict): + """ + Print weights in a nice format + Parameters + ---------- + weights: dict + weights to display. Keys are stocks. Values are either weights or values if -v specified + """ + + def pie_label(label_input): + """Function for autopct""" + return f"{label_input:.2f}" + + if not weights: + return + stocks = list(weights.keys()) + sizes = list(weights.values()) + fig, ax = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI) + if math.isclose(sum(sizes), 1, rel_tol=0.1): + wedges, texts, autotexts = ax.pie( + sizes, labels=stocks, autopct="%1.1f%%", textprops=dict(color="w") + ) + else: + wedges, texts, autotexts = ax.pie( + sizes, labels=stocks, autopct="", textprops=dict(color="w") + ) + for i, a in enumerate(autotexts): + a.set_text("{}".format(sizes[i])) + + ax.axis("equal") + ax.legend( + wedges, + stocks, + 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 gtff.USE_ION: + plt.ion() + plt.show() + print("") From 535e89f8b4fd07841d1b0ebcd4c08ecba475f77d Mon Sep 17 00:00:00 2001 From: Jamie Date: Tue, 13 Apr 2021 10:08:18 -0400 Subject: [PATCH 18/31] help out linter --- .../portfolio_optimization/port_opt_helper.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_helper.py b/gamestonk_terminal/portfolio_optimization/port_opt_helper.py index f8695de0bfa..407e20237c0 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_helper.py +++ b/gamestonk_terminal/portfolio_optimization/port_opt_helper.py @@ -72,17 +72,13 @@ def display_weights(weights: dict): def pie_chart_weights(weights: dict): """ - Print weights in a nice format + Show a pie chart of holdings Parameters ---------- weights: dict weights to display. Keys are stocks. Values are either weights or values if -v specified """ - def pie_label(label_input): - """Function for autopct""" - return f"{label_input:.2f}" - if not weights: return stocks = list(weights.keys()) From ac27fe2da76d4ebecb7e5f45eba94c729e773d04 Mon Sep 17 00:00:00 2001 From: Jamie Date: Tue, 13 Apr 2021 10:51:37 -0400 Subject: [PATCH 19/31] more linting --- .../portfolio_optimization/port_opt_helper.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_helper.py b/gamestonk_terminal/portfolio_optimization/port_opt_helper.py index 407e20237c0..743f44dfcb6 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_helper.py +++ b/gamestonk_terminal/portfolio_optimization/port_opt_helper.py @@ -31,7 +31,6 @@ def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFra """ stock_prices = yf.download(list_of_stocks, period=period, group_by="ticker") stock_closes = pd.DataFrame(index=stock_prices.index) - # process df for stock in list_of_stocks: stock_closes[stock] = stock_prices[stock]["Adj Close"] return stock_closes @@ -81,15 +80,16 @@ def pie_chart_weights(weights: dict): if not weights: return + stocks = list(weights.keys()) sizes = list(weights.values()) - fig, ax = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI) + _, ax = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI) if math.isclose(sum(sizes), 1, rel_tol=0.1): - wedges, texts, autotexts = ax.pie( + wedges, _, autotexts = ax.pie( sizes, labels=stocks, autopct="%1.1f%%", textprops=dict(color="w") ) else: - wedges, texts, autotexts = ax.pie( + wedges, _, autotexts = ax.pie( sizes, labels=stocks, autopct="", textprops=dict(color="w") ) for i, a in enumerate(autotexts): From 67028bd6e25e0bf2d5adcae0e05deb993a7d31a7 Mon Sep 17 00:00:00 2001 From: Jamie Date: Tue, 13 Apr 2021 11:13:36 -0400 Subject: [PATCH 20/31] finally figured out I needed to upgrade pyupgrade --- gamestonk_terminal/portfolio_optimization/port_opt_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_helper.py b/gamestonk_terminal/portfolio_optimization/port_opt_helper.py index 743f44dfcb6..809f99ffc12 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_helper.py +++ b/gamestonk_terminal/portfolio_optimization/port_opt_helper.py @@ -93,7 +93,7 @@ def pie_chart_weights(weights: dict): sizes, labels=stocks, autopct="", textprops=dict(color="w") ) for i, a in enumerate(autotexts): - a.set_text("{}".format(sizes[i])) + a.set_text(f"{sizes[i]}") ax.axis("equal") ax.legend( From 204140ab8070f19e611c58b0e02eec09f3018c68 Mon Sep 17 00:00:00 2001 From: Jamie Date: Tue, 13 Apr 2021 12:32:26 -0400 Subject: [PATCH 21/31] update readme with updated flags --- .../portfolio_optimization/README.md | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md index 6d3e96b5a09..c5fb0c4fe6f 100644 --- a/gamestonk_terminal/portfolio_optimization/README.md +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -42,20 +42,28 @@ Once your stocks are listed, you can select one of the options. * market cap weighted * dividend yield weighted ### equal weights +Returns an equally weighted portfolio where the weights are 1/number of stocks. ```` -equal_weight +equal_weight [-v --value VALUE] [--pie] ```` -Returns `{"Ticker" : 1/# of tickers}` +* -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. + ### market cap weighted +Returns portfolio values that are weighted by their relative Market Cap. ```` -mkt_cap +mkt_cap [-v --value VALUE] [--pie] ```` -Returns a dictionary where each weight is given as (Company Market Cap)/(Sum all included market caps) -###dividened yield weighted +* -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. + +###dividend yield weighted +Returns portfolio values that are weighted by relative Dividend Yield. ```` -div_yield +div_yield [-v --value VALUE] [--pie] ```` -Returns weights based on relative divident yield. +* -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. ## Mean Variance Optimization @@ -86,35 +94,43 @@ The sharpe ratio is defined as The usage is: ```` -max_sharpe [-p PERIOD] +max_sharpe [-p PERIOD] [-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. +* -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. ### min_vol This portfolio minimizes the total volatility, which also means it has the smallest returns among the efficient frontier. The usage is: ```` -min_vol [-p PERIOD] +min_vol [-p PERIOD] [-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. +* -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. ### eff_risk This portfolio maximizes the returns at a given risk tolerance The usage is: ```` -eff_risk [-p PERIOD] [-r --risk RISK_LEVEL] +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. +* -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. ### eff_ret This portfolio minimizes the risk at a given return level The usage is: ```` -eff_ret [-p PERIOD] [-r --return] +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. +* -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. From a7ed6d1f19f9084d33d45655277348fbafc756b8 Mon Sep 17 00:00:00 2001 From: Jamie Date: Tue, 13 Apr 2021 13:07:34 -0400 Subject: [PATCH 22/31] fixed where value wanst being used in some calculations --- .../portfolio_optimization/port_opt_api.py | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_api.py b/gamestonk_terminal/portfolio_optimization/port_opt_api.py index 3a120ec8eed..3fd0fe636e0 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_api.py +++ b/gamestonk_terminal/portfolio_optimization/port_opt_api.py @@ -259,7 +259,10 @@ def ef_portfolio(list_of_stocks: List[str], port_type: str, other_args: List[str stock_prices = process_stocks(list_of_stocks, period) ef = prepare_efficient_frontier(stock_prices) ef_sharpe = dict(ef.max_sharpe()) - weights = {key: round(value, 5) for key, value in ef_sharpe.items()} + weights = { + key: ns_parser.value * round(value, 5) + for key, value in ef_sharpe.items() + } if ns_parser.pie: pie_chart_weights(weights) print("") @@ -280,7 +283,10 @@ def ef_portfolio(list_of_stocks: List[str], port_type: str, other_args: List[str stock_prices = process_stocks(list_of_stocks, period) ef = prepare_efficient_frontier(stock_prices) ef_min_vol = dict(ef.min_volatility()) - weights = {key: round(value, 5) for key, value in ef_min_vol.items()} + 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("") @@ -305,7 +311,10 @@ def ef_portfolio(list_of_stocks: List[str], port_type: str, other_args: List[str 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: round(value, 5) for key, value in ef_eff_risk.items()} + 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("") @@ -329,7 +338,10 @@ def ef_portfolio(list_of_stocks: List[str], port_type: str, other_args: List[str 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: round(value, 5) for key, value in ef_eff_risk.items()} + 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("") From 940fa9465b51728e784ef62bb2dd069ad4dc9065 Mon Sep 17 00:00:00 2001 From: Jamie Date: Tue, 13 Apr 2021 13:23:11 -0400 Subject: [PATCH 23/31] fix bug where mkt_cap or div_yield would show error if not found in yfinance info --- gamestonk_terminal/portfolio_optimization/port_opt_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_api.py b/gamestonk_terminal/portfolio_optimization/port_opt_api.py index 3fd0fe636e0..8df994a7f83 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_api.py +++ b/gamestonk_terminal/portfolio_optimization/port_opt_api.py @@ -138,6 +138,8 @@ def property_weighting( return for stock in list_of_stocks: stock_prop = yf.Ticker(stock).info[property_type] + if stock_prop is None: + stock_prop = 0 prop[stock] = stock_prop prop_sum += stock_prop for k, v in prop.items(): From c18327dc1373205138daf001f67c4b80be2cfaf5 Mon Sep 17 00:00:00 2001 From: Jamie Date: Tue, 13 Apr 2021 13:43:06 -0400 Subject: [PATCH 24/31] added option to jump from scr to po. Just pass stocks spaced apart --- gamestonk_terminal/screener/README.md | 13 ++++++++++++- gamestonk_terminal/screener/screener_controller.py | 7 +++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/gamestonk_terminal/screener/README.md b/gamestonk_terminal/screener/README.md index b4f46eb644d..6271a7684db 100644 --- a/gamestonk_terminal/screener/README.md +++ b/gamestonk_terminal/screener/README.md @@ -23,7 +23,8 @@ This menu aims to filter stocks based on pre-specified preset filters, and the u * contains [Oversold (-s) signal example](#signal-oversold) * [signals](#signals) * view filter signals (e.g. -s top_gainers) [Finviz] - +* [po](#port_opt) + * go to the portfolio optimization menu ## view @@ -180,3 +181,13 @@ Prints list of available signals. [Source: Finviz] Captura de ecrã 2021-04-05, às 20 25 13 +## po +Goes to the portfolio menu with list of passed stocks. In order to pass the stocks, just type the tickers of interest, no commas. + +``` +usage: po ticker1 ticker2 ... +``` + +```` +example: po aapl msft tsla gme +```` \ No newline at end of file diff --git a/gamestonk_terminal/screener/screener_controller.py b/gamestonk_terminal/screener/screener_controller.py index cc34b3e80ab..76fd2fecb56 100644 --- a/gamestonk_terminal/screener/screener_controller.py +++ b/gamestonk_terminal/screener/screener_controller.py @@ -14,6 +14,7 @@ from gamestonk_terminal.helper_funcs import ( ) from gamestonk_terminal.screener import finviz_view from gamestonk_terminal.screener import yahoo_finance_view +from gamestonk_terminal.portfolio_optimization import po_controller class ScreenerController: @@ -34,6 +35,7 @@ class ScreenerController: "performance", "technical", "signals", + "po", ] def __init__(self): @@ -69,6 +71,8 @@ class ScreenerController: print("") print(" signals view filter signals (e.g. -s top_gainers)") print("") + print(" > po go to the portfolio optimization menu") + print("") @staticmethod def view_available_presets(other_args: List[str]): @@ -217,6 +221,9 @@ class ScreenerController: """Process signals command""" finviz_view.view_signals(other_args) + def call_po(self, other_args: List[str]): + return po_controller.menu(other_args) + def menu(): """Screener Menu""" From c36df9bdab98ab34ff67383ad0285ab90fa59f42 Mon Sep 17 00:00:00 2001 From: Jamie Date: Tue, 13 Apr 2021 13:45:49 -0400 Subject: [PATCH 25/31] added option to jump from scr to po. Just pass stocks spaced apart + black + spelling --- .../portfolio_optimization/port_opt_api.py | 10 +++++----- gamestonk_terminal/screener/screener_controller.py | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_api.py b/gamestonk_terminal/portfolio_optimization/port_opt_api.py index 8df994a7f83..c095ef3115a 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_api.py +++ b/gamestonk_terminal/portfolio_optimization/port_opt_api.py @@ -61,7 +61,7 @@ def equal_weight(list_of_stocks: List[str], other_args: List[str]): default=1, type=float, dest="value", - help="Portfolio amount to determine amount spent on each", + help="Amount to allocate to portfolio", ) parser.add_argument( "--pie", @@ -93,7 +93,7 @@ def property_weighting( list_of_stocks: List[str], property_type: str, other_args: List[str] ): """ - Property weighted portfolio where each weight is the relative fraction. Examples + Property weighted portfolio where each weight is the relative fraction. Parameters ---------- list_of_stocks: List[str] @@ -119,7 +119,7 @@ def property_weighting( default=1, type=float, dest="value", - help="Portfolio amount to determine amount spent on each", + help="Amount to allocate to portfolio", ) parser.add_argument( "--pie", @@ -213,7 +213,7 @@ def show_ef(list_of_stocks: List[str], other_args: List[str]): def ef_portfolio(list_of_stocks: List[str], port_type: str, other_args: List[str]): """ - Return a portfolio based on condition in port_type Currently defaulting to 3m of historical data + Return a portfolio based on condition in port_type Defaults to 3m of historical data Parameters ---------- list_of_stocks: List[str] @@ -241,7 +241,7 @@ def ef_portfolio(list_of_stocks: List[str], port_type: str, other_args: List[str "-v", "--value", dest="value", - help="Portfolio amount to determine amount spent on each", + help="Amount to allocate to portfolio", type=float, default=1.0, ) diff --git a/gamestonk_terminal/screener/screener_controller.py b/gamestonk_terminal/screener/screener_controller.py index 76fd2fecb56..0fe364750b0 100644 --- a/gamestonk_terminal/screener/screener_controller.py +++ b/gamestonk_terminal/screener/screener_controller.py @@ -222,6 +222,7 @@ class ScreenerController: finviz_view.view_signals(other_args) def call_po(self, other_args: List[str]): + """Call the portfolio optimization menu""" return po_controller.menu(other_args) From aeaf6b4e16c8b52bfd0b98c1ba1d055bfa003c06 Mon Sep 17 00:00:00 2001 From: Jamie Date: Tue, 13 Apr 2021 17:25:19 -0400 Subject: [PATCH 26/31] 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) From f33dddebc4b72312d716cd1e915cd27719c5917c Mon Sep 17 00:00:00 2001 From: Jamie Date: Wed, 14 Apr 2021 11:56:56 -0400 Subject: [PATCH 27/31] 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) From 0228fb17c46205857ca1a5e26cba9d327cea6758 Mon Sep 17 00:00:00 2001 From: Jamie Date: Wed, 14 Apr 2021 12:04:10 -0400 Subject: [PATCH 28/31] linting --- gamestonk_terminal/portfolio_optimization/README.md | 4 ++-- gamestonk_terminal/portfolio_optimization/optimizer_helper.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md index f0f1b62fc5b..bf96cf77629 100644 --- a/gamestonk_terminal/portfolio_optimization/README.md +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -153,7 +153,7 @@ 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 +so the number is your portfolio volatility * `sqrt(252`. This 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. ```` @@ -166,4 +166,4 @@ 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 +Note that since `AAPL` had zero allocation, it was omitted 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 e341f32ae24..750ab058262 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py @@ -123,7 +123,7 @@ def pie_chart_weights(weights: dict, optimizer: str, value: Optional[float]): f"{str(a)}: {str(round(100*b/total_size,3))[:4]}%" for a, b in zip(stocks, sizes) ] - + fig = plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI) if math.isclose(sum(sizes), 1, rel_tol=0.1): wedges, _, autotexts = plt.pie( sizes, From b14fee3d38b1a4384ffc3475a734a46c4b7b5c7f Mon Sep 17 00:00:00 2001 From: Jamie Date: Wed, 14 Apr 2021 12:10:41 -0400 Subject: [PATCH 29/31] more lint --- .../portfolio_optimization/optimizer_helper.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py index 750ab058262..43568468504 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py @@ -111,19 +111,22 @@ def pie_chart_weights(weights: dict, optimizer: str, value: Optional[float]): if not weights: return - stocks = np.array(list(weights.keys())) - sizes = np.array(list(weights.values())) + init_stocks = list(weights.keys()) + init_sizes = list(weights.values()) + stocks = [] + sizes = [] + for stock, size in zip(init_stocks, init_sizes): + if size > 0: + stocks.append(stock) + sizes.append(size) - 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) ] - fig = plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI) + plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI) if math.isclose(sum(sizes), 1, rel_tol=0.1): wedges, _, autotexts = plt.pie( sizes, From 7fdb601ac52e1a7b7b2dde1eb524c691c8732c3a Mon Sep 17 00:00:00 2001 From: Jamie Date: Wed, 14 Apr 2021 12:13:54 -0400 Subject: [PATCH 30/31] should be last lint --- .../portfolio_optimization/optimizer_helper.py | 2 +- .../portfolio_optimization/optimizer_view.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py index 43568468504..d3134829b70 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py @@ -97,7 +97,7 @@ def my_autopct(x): return "" -def pie_chart_weights(weights: dict, optimizer: str, value: Optional[float]): +def pie_chart_weights(weights: dict, optimizer: str, value: float): """ Show a pie chart of holdings Parameters diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py index 75f24bef38e..2014b9b369d 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, "equal", None) + pie_chart_weights(values, "equal", 0) 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, property_type, None) + pie_chart_weights(weights, property_type, 0) if property_type == "marketCap": print("Market Cap Weighted Portfolio: ") @@ -225,7 +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 + val = 0 print("Weights that maximize Sharpe Ratio:") elif port_type == "min_volatility": @@ -235,7 +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 + val = 0 print("Weights that minimize volatility") elif port_type == "eff_risk": From 41d99deda85ff4d547585882cb095b5fc1cdea22 Mon Sep 17 00:00:00 2001 From: Jamie Date: Wed, 14 Apr 2021 12:16:28 -0400 Subject: [PATCH 31/31] should be last lint..... --- gamestonk_terminal/portfolio_optimization/optimizer_helper.py | 2 +- gamestonk_terminal/portfolio_optimization/optimizer_view.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py index d3134829b70..3ca81f396a2 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py @@ -2,7 +2,7 @@ __docformat__ = "numpy" import argparse -from typing import List, Optional +from typing import List import math import pandas as pd import numpy as np diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py index 2014b9b369d..e9d74a76138 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -180,7 +180,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): Dictionary of weights where keys are the tickers. """ - parser = argparse.ArgumentParser(add_help=False, prog="port_type") + parser = argparse.ArgumentParser(add_help=False, prog=port_type) parser.add_argument( "-p",