From ccd322d6b60c9755154b3d86ed1b47f815c8dc12 Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 11 Apr 2021 16:15:11 -0400 Subject: [PATCH 001/112] 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 002/112] 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 003/112] 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 004/112] 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 005/112] 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 006/112] 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 007/112] 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 008/112] 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 009/112] 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 010/112] 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 011/112] 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 012/112] 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 013/112] 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 014/112] 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 015/112] 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 016/112] 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 017/112] 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 018/112] 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 019/112] 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 020/112] 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 021/112] 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 022/112] 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 023/112] 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 024/112] 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 025/112] 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 026/112] 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 32efa688cf5a005231ec4443cf3b527a7c2e36dc Mon Sep 17 00:00:00 2001 From: didier Date: Wed, 14 Apr 2021 02:01:57 +0100 Subject: [PATCH 027/112] Refactor options menu --- gamestonk_terminal/options/op_controller.py | 43 +-- gamestonk_terminal/options/volume.py | 176 ------------- gamestonk_terminal/options/volume_view.py | 273 ++++++++++++++++++++ terminal.py | 4 +- 4 files changed, 298 insertions(+), 198 deletions(-) delete mode 100644 gamestonk_terminal/options/volume.py create mode 100644 gamestonk_terminal/options/volume_view.py diff --git a/gamestonk_terminal/options/op_controller.py b/gamestonk_terminal/options/op_controller.py index 9459833b180..3600a50e2ba 100644 --- a/gamestonk_terminal/options/op_controller.py +++ b/gamestonk_terminal/options/op_controller.py @@ -9,7 +9,7 @@ import yfinance as yf from prompt_toolkit.completion import NestedCompleter from gamestonk_terminal.helper_funcs import get_flair, parse_known_args_and_warn from gamestonk_terminal import feature_flags as gtff -from gamestonk_terminal.options import volume +from gamestonk_terminal.options import volume_view from gamestonk_terminal.menu import session @@ -17,16 +17,15 @@ class OptionsController: """Options Controller class.""" # Command choices - CHOICES = ["help", "q", "quit", "exp", "volume"] + CHOICES = ["help", "q", "quit", "exp", "voi"] - def __init__( - self, - ticker: str, - ): + def __init__(self, ticker: str, last_adj_close_price: float): """Construct data.""" self.ticker = ticker - self.raw_data_options = yf.Ticker(self.ticker) - self.expiry_date = self.raw_data_options.options[0] + self.yf_ticker_data = yf.Ticker(self.ticker) + self.expiry_date = self.yf_ticker_data.options[0] + self.options = self.yf_ticker_data.option_chain(self.expiry_date) + self.last_adj_close_price = last_adj_close_price self.op_parser = argparse.ArgumentParser(add_help=False, prog="op") self.op_parser.add_argument( "cmd", @@ -48,7 +47,7 @@ class OptionsController: action="store", type=int, default=-1, - choices=range(len(self.raw_data_options.options)), + choices=range(len(self.yf_ticker_data.options)), help=f"Expiry date index for {self.ticker}.", ) @@ -64,12 +63,13 @@ class OptionsController: # Print possible expiry dates if ns_parser.n_date == -1: print("\nAvailable expiry dates:") - for i, d in enumerate(self.raw_data_options.options): + for i, d in enumerate(self.yf_ticker_data.options): print(f" {(2-len(str(i)))*' '}{i}. {d}") # It means an expiry date was correctly selected else: - self.expiry_date = self.raw_data_options.options[ns_parser.n_date] + self.expiry_date = self.yf_ticker_data.options[ns_parser.n_date] + self.options = self.yf_ticker_data.option_chain(self.expiry_date) print(f"\nSelected expiry date : {self.expiry_date}") except Exception as e: @@ -89,7 +89,7 @@ class OptionsController: print(f"Selected expiry date: {expiry_date}") print("") print(" exp see/set expiry date") - print(" volume plot options trading volume / open interest") + print(" voi volume + open interest options trading plot") print("") return @@ -121,26 +121,27 @@ class OptionsController: """Process Quit command - quit the program.""" return True - def call_volume(self, _): - """Process volume command.""" - volume.volume_graph( - self.raw_data_options, + def call_voi(self, other_args: List[str]): + """Process voi command.""" + volume_view.volume_open_interest_graph( + other_args, self.ticker, self.expiry_date, - volume_percentile_threshold=60, + self.last_adj_close_price, + self.options.calls, + self.options.puts, ) - print("") def call_exp(self, other_args: List[str]): """Process exp command.""" self.expiry_dates(self, other_args) -def menu(ticker: str): - """Options info Menu.""" +def menu(ticker: str, last_adj_close_price: float): + """ Options Menu. """ try: - op_controller = OptionsController(ticker) + op_controller = OptionsController(ticker, last_adj_close_price) op_controller.call_help(None) except IndexError: print("No options found for " + ticker) diff --git a/gamestonk_terminal/options/volume.py b/gamestonk_terminal/options/volume.py deleted file mode 100644 index 3b87ac8efe7..00000000000 --- a/gamestonk_terminal/options/volume.py +++ /dev/null @@ -1,176 +0,0 @@ -"""options info. [Source: Yahoo Finance].""" -from bisect import bisect_left -import seaborn as sns -import pandas as pd -import matplotlib.pyplot as plt -import numpy as np - -from gamestonk_terminal.helper_funcs import plot_autoscale -from gamestonk_terminal import config_plot as cfgPlot - - -def volume_graph(raw_data, ticker_name, exp_date, volume_percentile_threshold=50): - """Docstring make linter hap.""" - # SET VOLUME TO BE FILTERED, default = 50 - PERCENTILE_THRESHOLD = volume_percentile_threshold - - TICKER_NAME = ticker_name - raw_data_options = raw_data - EXP_DATE = exp_date - - # current stock price - spot = __get_current_spot(raw_data_options) - - calls = __parse_opt_data(raw_data_options, EXP_DATE) - puts = __parse_opt_data(raw_data_options, EXP_DATE, is_calls=False) - - calls = __add_max_pain_data(calls, spot) - - puts = __add_max_pain_data(puts, spot, is_calls=False) - - max_pain = __calc_max_pain(calls, puts) - - # Initialize the matplotlib figure - _, ax = plt.subplots(figsize=plot_autoscale(), dpi=cfgPlot.PLOT_DPI) - # _, ax = plt.subplots(figsize=(12, 10)) - - # make x axis symmetric - axis_origin = max(abs(max(puts["oi+v"])), abs(max(calls["oi+v"]))) - ax.set_xlim(-axis_origin, +axis_origin) - - VOLUME_THRESHOLD = np.percentile(calls["oi+v"], PERCENTILE_THRESHOLD) - - sns.set_style(style="darkgrid") - - g = sns.barplot( - x="oi+v", - y="strike", - data=calls[calls["oi+v"] > VOLUME_THRESHOLD], - label="Calls: Open Interest", - color="lightgreen", - orient="h", - ) - - g = sns.barplot( - x="volume", - y="strike", - data=calls[calls["oi+v"] > VOLUME_THRESHOLD], - label="Calls: Volume", - color="green", - orient="h", - ) - - g = sns.barplot( - x="oi+v", - y="strike", - data=puts[puts["oi+v"] < -VOLUME_THRESHOLD], - label="Puts: Open Interest", - color="pink", - orient="h", - ) - - g = sns.barplot( - x="volume", - y="strike", - data=puts[puts["oi+v"] < -VOLUME_THRESHOLD], - label="Puts: Volume", - color="red", - orient="h", - ) - - # draw spot line - s = [float(strike.get_text()) for strike in ax.get_yticklabels()] - spot_index = bisect_left(s, spot) # find where the spot is on the graph - spot_line = ax.axhline(spot_index, ls="--", color="dodgerblue", alpha=0.3) - - # draw max pain line - max_pain_index = bisect_left(s, max_pain) - max_pain_line = ax.axhline(max_pain_index, ls="-", color="black", alpha=0.3) - max_pain_line.set_linewidth(5) - - # ax.axhline(max_pain_index, ls='--') - # format ticklabels without - for puts - g.set_xticks(g.get_xticks()) - xlabels = [f"{x:,.0f}".replace("-", "") for x in g.get_xticks()] - g.set_xticklabels(xlabels) - - plt.title( - f"{TICKER_NAME.upper()} volumes for {EXP_DATE} (open interest displayed only during market hours)" - ) - ax.invert_yaxis() - - # ax.spines['left'].set_position('center') - - _ = ax.legend() - handles, _ = ax.get_legend_handles_labels() - handles.append(spot_line) - handles.append(max_pain_line) - - # create legend labels + add to graph - labels = [ - "Calls open interest", - "Calls volume ", - "Puts open interest", - "Puts volume", - "Current stock price", - f"Max pain = {max_pain}", - ] - - plt.legend(handles=handles[:], labels=labels) - sns.despine(left=True, bottom=True) - plt.show() - - -def __add_max_pain_data(df, spot, is_calls=True): - # max pain parsing + calculation - df["spot"] = round(spot, 2) - if is_calls: - df["dv"] = spot - df.index - else: - df["dv"] = df.index - spot - df["dv"] = df["dv"].apply(lambda x: max(0, x)) - df["dv"] = abs(df["dv"] * df["volume"]) - return df - - -def __calc_max_pain(calls, puts): - - df = pd.merge(calls, puts, left_index=True, right_index=True) - df["dv"] = round(df["dv_x"] + df["dv_y"], 2) - - max_pain = df["dv"].idxmax() - return max_pain - - -def __parse_opt_data(raw_data_options, exp_date, is_calls=True): - # get option chain for specific expiration - opt = raw_data_options.option_chain(exp_date) - - # PARSE DATA - if is_calls: - option_data = opt.calls - flag = "calls" - else: - option_data = opt.puts - flag = "puts" - - data = option_data.pivot_table( - index="strike", values=["volume", "openInterest"], aggfunc="sum" - ).reindex() - - data["strike"] = data.index - data["type"] = flag - - if is_calls: - data["openInterest"] = data["openInterest"] - data["volume"] = data["volume"] - else: - data["openInterest"] = -data["openInterest"] - data["volume"] = -data["volume"] - - data["oi+v"] = data["openInterest"] + data["volume"] - return data - - -def __get_current_spot(raw_data_options): - return raw_data_options.history().tail(1)["Close"].iloc[0] diff --git a/gamestonk_terminal/options/volume_view.py b/gamestonk_terminal/options/volume_view.py new file mode 100644 index 00000000000..26db5219a27 --- /dev/null +++ b/gamestonk_terminal/options/volume_view.py @@ -0,0 +1,273 @@ +""" Volume view """ +__docformat__ = "numpy" + +from bisect import bisect_left +import seaborn as sns +from typing import List +import argparse +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np +import yfinance + +from gamestonk_terminal.helper_funcs import ( + plot_autoscale, + check_non_negative, + parse_known_args_and_warn, +) +from gamestonk_terminal import config_plot as cfgPlot +from gamestonk_terminal import feature_flags as gtff + + +def volume_open_interest_graph( + other_args: List[str], + ticker: str, + exp_date: str, + last_adj_close_price: float, + op_calls: pd.DataFrame, + op_puts: pd.DataFrame, +): + """Options volume and open interest + + Parameters + ---------- + other_args : List[str] + Command line arguments to be processed with argparse + ticker : str + Main ticker to compare income + exp_date : str + Expiry date of the option + last_adj_close_price: float + Last adjusted closing price + op_calls: pd.DataFrame + Option data calls + op_puts: pd.DataFrame + Option data puts + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="voi", + description=""" + Plots Volume + Open Interest of calls vs puts. [Source: Yahoo Finance] + """, + ) + parser.add_argument( + "-v", + "--minv", + dest="min_vol", + type=check_non_negative, + default=-1, + help="minimum volume (considering open interest) threshold of the plot.", + ) + parser.add_argument( + "-m", + "--min", + dest="min_sp", + type=check_non_negative, + default=-1, + help="minimum strike price to consider in the plot.", + ) + parser.add_argument( + "-M", + "--max", + dest="max_sp", + type=check_non_negative, + default=-1, + help="maximum strike price to consider in the plot.", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + df_calls, df_puts, max_pain = get_calls_puts_maxpain( + op_calls, op_puts, last_adj_close_price + ) + + if ( + ns_parser.min_vol == -1 + and ns_parser.min_sp == -1 + and ns_parser.max_sp == -1 + ): + # If no argument provided, we use the percentile 50 to get 50% of upper volume data + volume_percentile_threshold = 50 + min_vol_calls = np.percentile(df_calls["oi+v"], volume_percentile_threshold) + min_vol_puts = np.percentile(df_puts["oi+v"], volume_percentile_threshold) + + df_calls = df_calls[df_calls["oi+v"] > min_vol_calls] + df_puts = df_puts[df_puts["oi+v"] < min_vol_puts] + + else: + if ns_parser.min_vol > -1: + df_calls = df_calls[df_calls["oi+v"] > ns_parser.min_vol] + df_puts = df_puts[df_puts["oi+v"] < -ns_parser.min_vol] + + if ns_parser.min_sp > -1: + df_calls = df_calls[df_calls["strike"] > ns_parser.min_sp] + df_puts = df_puts[df_puts["strike"] > ns_parser.min_sp] + + if ns_parser.max_sp > -1: + df_calls = df_calls[df_calls["strike"] < ns_parser.max_sp] + df_puts = df_puts[df_puts["strike"] < ns_parser.max_sp] + + if df_calls.empty and df_puts.empty: + print( + "The filtering applied is too strong, there is no data available for such conditions.\n" + ) + return + + # Initialize the matplotlib figure + _, ax = plt.subplots(figsize=plot_autoscale(), dpi=cfgPlot.PLOT_DPI) + + # make x axis symmetric + axis_origin = max(abs(max(df_puts["oi+v"])), abs(max(df_calls["oi+v"]))) + ax.set_xlim(-axis_origin, +axis_origin) + + sns.set_style(style="darkgrid") + + g = sns.barplot( + x="oi+v", + y="strike", + data=df_calls, + label="Calls: Open Interest", + color="lightgreen", + orient="h", + ) + + g = sns.barplot( + x="volume", + y="strike", + data=df_calls, + label="Calls: Volume", + color="green", + orient="h", + ) + + g = sns.barplot( + x="oi+v", + y="strike", + data=df_puts, + label="Puts: Open Interest", + color="pink", + orient="h", + ) + + g = sns.barplot( + x="volume", + y="strike", + data=df_puts, + label="Puts: Volume", + color="red", + orient="h", + ) + + # draw spot line + s = [float(strike.get_text()) for strike in ax.get_yticklabels()] + spot_index = bisect_left( + s, last_adj_close_price + ) # find where the spot is on the graph + spot_line = ax.axhline(spot_index, ls="--", color="dodgerblue", alpha=0.3) + + # draw max pain line + max_pain_index = bisect_left(s, max_pain) + max_pain_line = ax.axhline(max_pain_index, ls="-", color="black", alpha=0.3) + max_pain_line.set_linewidth(5) + + # format ticklabels without - for puts + g.set_xticks(g.get_xticks()) + xlabels = [f"{x:,.0f}".replace("-", "") for x in g.get_xticks()] + g.set_xticklabels(xlabels) + + plt.title( + f"{ticker} volumes for {exp_date} (open interest displayed only during market hours)" + ) + ax.invert_yaxis() + + _ = ax.legend() + handles, _ = ax.get_legend_handles_labels() + handles.append(spot_line) + handles.append(max_pain_line) + + # create legend labels + add to graph + labels = [ + "Calls open interest", + "Calls volume ", + "Puts open interest", + "Puts volume", + "Current stock price", + f"Max pain = {max_pain}", + ] + + plt.legend(handles=handles[:], labels=labels) + sns.despine(left=True, bottom=True) + + if gtff.USE_ION: + plt.ion() + plt.show() + + print("") + + except Exception as e: + print(e, "\n") + return + + +def get_calls_puts_maxpain( + op_calls: pd.DataFrame, op_puts: pd.DataFrame, last_adj_close_price: float +): + """Get calls and puts dataframes, and max pain + + Parameters + ---------- + op_calls: pd.DataFrame + Option data calls + op_puts: pd.DataFrame + Option data puts + last_adj_close_price: float + Last adjusted closing price + + Returns + ---------- + pd.DataFrame + Processed calls dataframe + pd.DataFrame + Processed puts dataframe + float + Max pain + """ + # Process Calls Data + df_calls = op_calls.pivot_table( + index="strike", values=["volume", "openInterest"], aggfunc="sum" + ).reindex() + df_calls["strike"] = df_calls.index + df_calls["type"] = "calls" + df_calls["openInterest"] = df_calls["openInterest"] + df_calls["volume"] = df_calls["volume"] + df_calls["oi+v"] = df_calls["openInterest"] + df_calls["volume"] + df_calls["spot"] = round(last_adj_close_price, 2) + df_calls["dv"] = last_adj_close_price - df_calls.index + df_calls["dv"] = df_calls["dv"].apply(lambda x: max(0, x)) + df_calls["dv"] = abs(df_calls["dv"] * df_calls["volume"]) + + # Process Puts Data + df_puts = op_puts.pivot_table( + index="strike", values=["volume", "openInterest"], aggfunc="sum" + ).reindex() + df_puts["strike"] = df_puts.index + df_puts["type"] = "puts" + df_puts["openInterest"] = df_puts["openInterest"] + df_puts["volume"] = -df_puts["volume"] + df_puts["openInterest"] = -df_puts["openInterest"] + df_puts["oi+v"] = df_puts["openInterest"] + df_puts["volume"] + df_puts["spot"] = round(last_adj_close_price, 2) + df_puts["dv"] = df_puts.index - last_adj_close_price + df_puts["dv"] = df_puts["dv"].apply(lambda x: max(0, x)) + df_puts["dv"] = abs(df_puts["dv"] * df_puts["volume"]) + + # Get max pain + df_opt = pd.merge(df_calls, df_puts, left_index=True, right_index=True) + df_opt["dv"] = round(df_opt["dv_x"] + df_opt["dv_y"], 2) + max_pain = df_opt["dv"].idxmax() + + return df_calls, df_puts, max_pain diff --git a/terminal.py b/terminal.py index f2441ccbdab..cfcc432bb04 100644 --- a/terminal.py +++ b/terminal.py @@ -231,7 +231,9 @@ def main(): b_quit = eda_controller.menu(df_stock, s_ticker, s_start, s_interval) elif ns_known_args.opt == "op": - b_quit = op_controller.menu(s_ticker) + b_quit = op_controller.menu( + s_ticker, df_stock["5. adjusted close"].values[-1] + ) elif ns_known_args.opt == "fred": b_quit = fred_controller.menu() From e0e15abeba714ee285763ba006b9ccae2a6b033a Mon Sep 17 00:00:00 2001 From: didier Date: Wed, 14 Apr 2021 02:04:14 +0100 Subject: [PATCH 028/112] remove unused import --- gamestonk_terminal/options/volume_view.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gamestonk_terminal/options/volume_view.py b/gamestonk_terminal/options/volume_view.py index 26db5219a27..e4aadb946a3 100644 --- a/gamestonk_terminal/options/volume_view.py +++ b/gamestonk_terminal/options/volume_view.py @@ -8,7 +8,6 @@ import argparse import pandas as pd import matplotlib.pyplot as plt import numpy as np -import yfinance from gamestonk_terminal.helper_funcs import ( plot_autoscale, From ce0cc6a6588832bbac4830c963ee1519b56f4dd1 Mon Sep 17 00:00:00 2001 From: didier Date: Wed, 14 Apr 2021 02:27:22 +0100 Subject: [PATCH 029/112] add vcalls and vputs commands to options menu --- gamestonk_terminal/options/op_controller.py | 30 ++- gamestonk_terminal/options/volume_view.py | 195 ++++++++++++++++---- 2 files changed, 188 insertions(+), 37 deletions(-) diff --git a/gamestonk_terminal/options/op_controller.py b/gamestonk_terminal/options/op_controller.py index 3600a50e2ba..01eb7e0df67 100644 --- a/gamestonk_terminal/options/op_controller.py +++ b/gamestonk_terminal/options/op_controller.py @@ -17,7 +17,7 @@ class OptionsController: """Options Controller class.""" # Command choices - CHOICES = ["help", "q", "quit", "exp", "voi"] + CHOICES = ["help", "q", "quit", "exp", "voi", "vcalls", "vputs"] def __init__(self, ticker: str, last_adj_close_price: float): """Construct data.""" @@ -90,6 +90,8 @@ class OptionsController: print("") print(" exp see/set expiry date") print(" voi volume + open interest options trading plot") + print(" vcalls calls volume + open interest plot") + print(" vputs puts volume + open interest plot") print("") return @@ -121,9 +123,13 @@ class OptionsController: """Process Quit command - quit the program.""" return True + def call_exp(self, other_args: List[str]): + """Process exp command.""" + self.expiry_dates(self, other_args) + def call_voi(self, other_args: List[str]): """Process voi command.""" - volume_view.volume_open_interest_graph( + volume_view.plot_volume_open_interest( other_args, self.ticker, self.expiry_date, @@ -132,9 +138,23 @@ class OptionsController: self.options.puts, ) - def call_exp(self, other_args: List[str]): - """Process exp command.""" - self.expiry_dates(self, other_args) + def call_vcalls(self, other_args: List[str]): + """Process vcalls command.""" + volume_view.plot_calls_volume_open_interest( + other_args, + self.ticker, + self.last_adj_close_price, + self.options.calls, + ) + + def call_vputs(self, other_args: List[str]): + """Process vcalls command.""" + volume_view.plot_puts_volume_open_interest( + other_args, + self.ticker, + self.last_adj_close_price, + self.options.puts, + ) def menu(ticker: str, last_adj_close_price: float): diff --git a/gamestonk_terminal/options/volume_view.py b/gamestonk_terminal/options/volume_view.py index e4aadb946a3..4a84471e04e 100644 --- a/gamestonk_terminal/options/volume_view.py +++ b/gamestonk_terminal/options/volume_view.py @@ -18,7 +18,7 @@ from gamestonk_terminal import config_plot as cfgPlot from gamestonk_terminal import feature_flags as gtff -def volume_open_interest_graph( +def plot_volume_open_interest( other_args: List[str], ticker: str, exp_date: str, @@ -235,38 +235,169 @@ def get_calls_puts_maxpain( float Max pain """ - # Process Calls Data - df_calls = op_calls.pivot_table( - index="strike", values=["volume", "openInterest"], aggfunc="sum" - ).reindex() - df_calls["strike"] = df_calls.index - df_calls["type"] = "calls" - df_calls["openInterest"] = df_calls["openInterest"] - df_calls["volume"] = df_calls["volume"] - df_calls["oi+v"] = df_calls["openInterest"] + df_calls["volume"] - df_calls["spot"] = round(last_adj_close_price, 2) - df_calls["dv"] = last_adj_close_price - df_calls.index - df_calls["dv"] = df_calls["dv"].apply(lambda x: max(0, x)) - df_calls["dv"] = abs(df_calls["dv"] * df_calls["volume"]) + df_calls = pd.DataFrame() + df_puts = pd.DataFrame() + max_pain = 0 - # Process Puts Data - df_puts = op_puts.pivot_table( - index="strike", values=["volume", "openInterest"], aggfunc="sum" - ).reindex() - df_puts["strike"] = df_puts.index - df_puts["type"] = "puts" - df_puts["openInterest"] = df_puts["openInterest"] - df_puts["volume"] = -df_puts["volume"] - df_puts["openInterest"] = -df_puts["openInterest"] - df_puts["oi+v"] = df_puts["openInterest"] + df_puts["volume"] - df_puts["spot"] = round(last_adj_close_price, 2) - df_puts["dv"] = df_puts.index - last_adj_close_price - df_puts["dv"] = df_puts["dv"].apply(lambda x: max(0, x)) - df_puts["dv"] = abs(df_puts["dv"] * df_puts["volume"]) + if not op_calls.empty: + # Process Calls Data + df_calls = op_calls.pivot_table( + index="strike", values=["volume", "openInterest"], aggfunc="sum" + ).reindex() + df_calls["strike"] = df_calls.index + df_calls["type"] = "calls" + df_calls["openInterest"] = df_calls["openInterest"] + df_calls["volume"] = df_calls["volume"] + df_calls["oi+v"] = df_calls["openInterest"] + df_calls["volume"] + df_calls["spot"] = round(last_adj_close_price, 2) + df_calls["dv"] = last_adj_close_price - df_calls.index + df_calls["dv"] = df_calls["dv"].apply(lambda x: max(0, x)) + df_calls["dv"] = abs(df_calls["dv"] * df_calls["volume"]) - # Get max pain - df_opt = pd.merge(df_calls, df_puts, left_index=True, right_index=True) - df_opt["dv"] = round(df_opt["dv_x"] + df_opt["dv_y"], 2) - max_pain = df_opt["dv"].idxmax() + if not op_puts.empty: + # Process Puts Data + df_puts = op_puts.pivot_table( + index="strike", values=["volume", "openInterest"], aggfunc="sum" + ).reindex() + df_puts["strike"] = df_puts.index + df_puts["type"] = "puts" + df_puts["openInterest"] = df_puts["openInterest"] + df_puts["volume"] = -df_puts["volume"] + df_puts["openInterest"] = -df_puts["openInterest"] + df_puts["oi+v"] = df_puts["openInterest"] + df_puts["volume"] + df_puts["spot"] = round(last_adj_close_price, 2) + df_puts["dv"] = df_puts.index - last_adj_close_price + df_puts["dv"] = df_puts["dv"].apply(lambda x: max(0, x)) + df_puts["dv"] = abs(df_puts["dv"] * df_puts["volume"]) + + if not op_calls.empty and not op_puts.empty: + # Get max pain + df_opt = pd.merge(df_calls, df_puts, left_index=True, right_index=True) + df_opt["dv"] = round(df_opt["dv_x"] + df_opt["dv_y"], 2) + max_pain = df_opt["dv"].idxmax() return df_calls, df_puts, max_pain + + +def plot_calls_volume_open_interest( + other_args: List[str], + ticker: str, + last_adj_close_price: float, + op_calls: pd.DataFrame, +): + """Options volume and open interest + + Parameters + ---------- + other_args : List[str] + Command line arguments to be processed with argparse + ticker : str + Main ticker to compare income + last_adj_close_price: float + Last adjusted closing price + op_calls: pd.DataFrame + Option data calls + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="vcalls", + description=""" + Plots Calls Volume + Open Interest. [Source: Yahoo Finance] + """, + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + df_calls, _, _ = get_calls_puts_maxpain( + op_calls, pd.DataFrame(), last_adj_close_price + ) + + plt.figure(figsize=plot_autoscale(), dpi=cfgPlot.PLOT_DPI) + + plt.axvline(last_adj_close_price, lw=2) + plt.bar( + df_calls["strike"], + df_calls["volume"] + df_calls["openInterest"], + color="lightgreen", + ) + plt.bar(df_calls["strike"], df_calls["volume"], color="green") + + plt.title("Calls Volume") + plt.legend(["Stock Price", "Open Interest", "Volume"]) + plt.xlabel("Strike Price") + plt.ylabel("Volume") + + if gtff.USE_ION: + plt.ion() + plt.show() + + print("") + + except Exception as e: + print(e, "\n") + return + + +def plot_puts_volume_open_interest( + other_args: List[str], + ticker: str, + last_adj_close_price: float, + op_puts: pd.DataFrame, +): + """Options volume and open interest + + Parameters + ---------- + other_args : List[str] + Command line arguments to be processed with argparse + ticker : str + Main ticker to compare income + last_adj_close_price: float + Last adjusted closing price + op_puts: pd.DataFrame + Option data puts + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="vputs", + description=""" + Plots Puts Volume + Open Interest. [Source: Yahoo Finance] + """, + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + _, df_puts, _ = get_calls_puts_maxpain( + pd.DataFrame(), op_puts, last_adj_close_price + ) + + plt.figure(figsize=plot_autoscale(), dpi=cfgPlot.PLOT_DPI) + + plt.axvline(last_adj_close_price, lw=2) + plt.bar( + df_puts["strike"], + -df_puts["volume"] - df_puts["openInterest"], + color="pink", + ) + plt.bar(df_puts["strike"], -df_puts["volume"], color="red") + + plt.title("Puts Volume") + plt.legend(["Stock Price", "Open Interest", "Volume"]) + plt.xlabel("Strike Price") + plt.ylabel("Volume") + + if gtff.USE_ION: + plt.ion() + plt.show() + + print("") + + except Exception as e: + print(e, "\n") + return From f33dddebc4b72312d716cd1e915cd27719c5917c Mon Sep 17 00:00:00 2001 From: Jamie Date: Wed, 14 Apr 2021 11:56:56 -0400 Subject: [PATCH 030/112] 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 031/112] 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 032/112] 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 033/112] 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 034/112] 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", From 14feae913c2c76fa4744229ef7b56d4b3405a368 Mon Sep 17 00:00:00 2001 From: didier Date: Wed, 14 Apr 2021 21:45:13 +0100 Subject: [PATCH 035/112] improve current options plots --- gamestonk_terminal/options/op_controller.py | 2 + gamestonk_terminal/options/volume_view.py | 64 ++++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/options/op_controller.py b/gamestonk_terminal/options/op_controller.py index 01eb7e0df67..e373ba1f677 100644 --- a/gamestonk_terminal/options/op_controller.py +++ b/gamestonk_terminal/options/op_controller.py @@ -143,6 +143,7 @@ class OptionsController: volume_view.plot_calls_volume_open_interest( other_args, self.ticker, + self.expiry_date, self.last_adj_close_price, self.options.calls, ) @@ -152,6 +153,7 @@ class OptionsController: volume_view.plot_puts_volume_open_interest( other_args, self.ticker, + self.expiry_date, self.last_adj_close_price, self.options.puts, ) diff --git a/gamestonk_terminal/options/volume_view.py b/gamestonk_terminal/options/volume_view.py index 4a84471e04e..fed8eb63c6f 100644 --- a/gamestonk_terminal/options/volume_view.py +++ b/gamestonk_terminal/options/volume_view.py @@ -282,6 +282,7 @@ def get_calls_puts_maxpain( def plot_calls_volume_open_interest( other_args: List[str], ticker: str, + exp_date: str, last_adj_close_price: float, op_calls: pd.DataFrame, ): @@ -293,6 +294,8 @@ def plot_calls_volume_open_interest( Command line arguments to be processed with argparse ticker : str Main ticker to compare income + exp_date : str + Expiry date of the option last_adj_close_price: float Last adjusted closing price op_calls: pd.DataFrame @@ -305,6 +308,22 @@ def plot_calls_volume_open_interest( Plots Calls Volume + Open Interest. [Source: Yahoo Finance] """, ) + parser.add_argument( + "-m", + "--min", + dest="min_sp", + type=check_non_negative, + default=-1, + help="minimum strike price to consider in the plot.", + ) + parser.add_argument( + "-M", + "--max", + dest="max_sp", + type=check_non_negative, + default=-1, + help="maximum strike price to consider in the plot.", + ) try: ns_parser = parse_known_args_and_warn(parser, other_args) @@ -315,6 +334,16 @@ def plot_calls_volume_open_interest( op_calls, pd.DataFrame(), last_adj_close_price ) + if ns_parser.min_sp == -1: + min_strike = np.percentile(df_calls["strike"], 25) + else: + min_strike = ns_parser.min_sp + + if ns_parser.max_sp == -1: + max_strike = np.percentile(df_calls["strike"], 75) + else: + max_strike = ns_parser.max_sp + plt.figure(figsize=plot_autoscale(), dpi=cfgPlot.PLOT_DPI) plt.axvline(last_adj_close_price, lw=2) @@ -325,10 +354,11 @@ def plot_calls_volume_open_interest( ) plt.bar(df_calls["strike"], df_calls["volume"], color="green") - plt.title("Calls Volume") + plt.title(f"{ticker} calls volumes for {exp_date} ") plt.legend(["Stock Price", "Open Interest", "Volume"]) plt.xlabel("Strike Price") plt.ylabel("Volume") + plt.xlim([min_strike, max_strike]) if gtff.USE_ION: plt.ion() @@ -344,6 +374,7 @@ def plot_calls_volume_open_interest( def plot_puts_volume_open_interest( other_args: List[str], ticker: str, + exp_date: str, last_adj_close_price: float, op_puts: pd.DataFrame, ): @@ -355,6 +386,8 @@ def plot_puts_volume_open_interest( Command line arguments to be processed with argparse ticker : str Main ticker to compare income + exp_date : str + Expiry date of the option last_adj_close_price: float Last adjusted closing price op_puts: pd.DataFrame @@ -367,6 +400,22 @@ def plot_puts_volume_open_interest( Plots Puts Volume + Open Interest. [Source: Yahoo Finance] """, ) + parser.add_argument( + "-m", + "--min", + dest="min_sp", + type=check_non_negative, + default=-1, + help="minimum strike price to consider in the plot.", + ) + parser.add_argument( + "-M", + "--max", + dest="max_sp", + type=check_non_negative, + default=-1, + help="maximum strike price to consider in the plot.", + ) try: ns_parser = parse_known_args_and_warn(parser, other_args) @@ -377,6 +426,16 @@ def plot_puts_volume_open_interest( pd.DataFrame(), op_puts, last_adj_close_price ) + if ns_parser.min_sp == -1: + min_strike = np.percentile(df_puts["strike"], 25) + else: + min_strike = ns_parser.min_sp + + if ns_parser.max_sp == -1: + max_strike = np.percentile(df_puts["strike"], 75) + else: + max_strike = ns_parser.max_sp + plt.figure(figsize=plot_autoscale(), dpi=cfgPlot.PLOT_DPI) plt.axvline(last_adj_close_price, lw=2) @@ -387,10 +446,11 @@ def plot_puts_volume_open_interest( ) plt.bar(df_puts["strike"], -df_puts["volume"], color="red") - plt.title("Puts Volume") + plt.title(f"{ticker} puts volumes for {exp_date} ") plt.legend(["Stock Price", "Open Interest", "Volume"]) plt.xlabel("Strike Price") plt.ylabel("Volume") + plt.xlim([min_strike, max_strike]) if gtff.USE_ION: plt.ion() From 1b16cd18806a96813059c33ed56be53f81f1a64a Mon Sep 17 00:00:00 2001 From: didier Date: Wed, 14 Apr 2021 23:13:43 +0100 Subject: [PATCH 036/112] improve vcalls and vputs default plots --- gamestonk_terminal/options/volume_view.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gamestonk_terminal/options/volume_view.py b/gamestonk_terminal/options/volume_view.py index fed8eb63c6f..a20e4cafc54 100644 --- a/gamestonk_terminal/options/volume_view.py +++ b/gamestonk_terminal/options/volume_view.py @@ -335,12 +335,12 @@ def plot_calls_volume_open_interest( ) if ns_parser.min_sp == -1: - min_strike = np.percentile(df_calls["strike"], 25) + min_strike = 0.75 * last_adj_close_price else: min_strike = ns_parser.min_sp if ns_parser.max_sp == -1: - max_strike = np.percentile(df_calls["strike"], 75) + max_strike = 1.25 * last_adj_close_price else: max_strike = ns_parser.max_sp @@ -427,12 +427,12 @@ def plot_puts_volume_open_interest( ) if ns_parser.min_sp == -1: - min_strike = np.percentile(df_puts["strike"], 25) + min_strike = 0.75 * last_adj_close_price else: min_strike = ns_parser.min_sp if ns_parser.max_sp == -1: - max_strike = np.percentile(df_puts["strike"], 75) + max_strike = 1.25 * last_adj_close_price else: max_strike = ns_parser.max_sp From 33a4b64b7e3129104b4c913ce70f7b03e9b17aab Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Thu, 15 Apr 2021 14:43:02 +0200 Subject: [PATCH 037/112] add market_news and news_article to disc menu market_news latest news [SeekingAlpha] news_article news article [SeekingAlpha] --- .../discovery/disc_controller.py | 12 +++ .../discovery/seeking_alpha_model.py | 93 +++++++++++++++++++ .../discovery/seeking_alpha_view.py | 70 ++++++++++++++ 3 files changed, 175 insertions(+) diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index 07bb33a3302..fa0f893232f 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -46,6 +46,8 @@ class DiscoveryController: "valuation", "performance", "spectrum", + "market_news", + "news_article" ] def __init__(self): @@ -87,6 +89,8 @@ class DiscoveryController: print(" valuation valuation of sectors, industry, country [Finviz]") print(" performance performance of sectors, industry, country [Finviz]") print(" spectrum spectrum of sectors, industry, country [Finviz]") + print(" market_news latest news [SeekingAlpha]") + print(" news_article news article [SeekingAlpha]") print("") def switch(self, an_input: str): @@ -187,6 +191,14 @@ class DiscoveryController: other_args, "spectrum" ) + def call_market_news(self, other_args: List[str]): + """Process market_news command""" + seeking_alpha_view.articles_list_view(other_args) + + def call_news_article(self, other_args: List[str]): + """Process news_article command""" + seeking_alpha_view.news_article_view(other_args) + def menu(): """Discovery Menu""" diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index 59f63e44ae6..06ec76e8b15 100755 --- a/gamestonk_terminal/discovery/seeking_alpha_model.py +++ b/gamestonk_terminal/discovery/seeking_alpha_model.py @@ -67,3 +67,96 @@ def get_next_earnings(pages: int) -> DataFrame: df_earnings = df_earnings.set_index("Date") return df_earnings + + +def get_articles_html(url_articles: str) -> str: + """Wraps HTTP requests.get for testibility + + Parameters + ---------- + url_articles : str + Articles URL + + Returns + ------- + str + HTML page of articles + """ + articles_html = requests.get( + url_articles, headers={"User-Agent": get_user_agent()} + ).text + + return articles_html + + +def get_article_list(pages: int) -> list: + """Returns a list of recent articles + + Parameters + ---------- + pages : int + Number of pages + + Returns + ------- + list + Latest articles list + """ + + articles = list() + url_articles = "https://seekingalpha.com/market-news" + for idx in range(0, pages): + text_soup_articles = BeautifulSoup( + get_articles_html(url_articles), + "lxml", + ) + + for item_row in text_soup_articles.findAll("li", {"class": "item"}): + item = item_row.find("a", {"class": "add-source-assigned"}) + if item is None: + continue + article_url = item['href'] + if not article_url.startswith('/news/'): + continue + article_id = article_url.split('/')[2].split('-')[0] + articles.append({ + 'title': item.text, + 'date': item_row['data-last-date'], + 'url': "https://seekingalpha.com"+article_url, + 'id': article_id + }) + + url_articles = ( + f"https://seekingalpha.com/market-news/{idx+1}" + ) + + return articles + + +def get_article_data(article_id: int) -> dict: + """Returns an article + + Parameters + ---------- + article_id : int + Article ID + + Returns + ------- + dict + Article data + """ + + article_url = f"https://seekingalpha.com/api/v3/news/{article_id}" + response = requests.get(article_url, headers={"User-Agent": get_user_agent()}) + jdata = response.json() + content = jdata['data']['attributes']['content'].replace('', '\n') + content = BeautifulSoup(content, features="html.parser").get_text() + + article = { + 'title': jdata['data']['attributes']['title'], + 'date': jdata['data']['attributes']['lastModified'], + 'content': content + } + + return article diff --git a/gamestonk_terminal/discovery/seeking_alpha_view.py b/gamestonk_terminal/discovery/seeking_alpha_view.py index 2965bf79cce..f54714b11cc 100644 --- a/gamestonk_terminal/discovery/seeking_alpha_view.py +++ b/gamestonk_terminal/discovery/seeking_alpha_view.py @@ -65,3 +65,73 @@ def earnings_release_dates_view(other_args: List[str]): ].to_string(index=False, header=False) ) print("") + + +def articles_list_view(other_args: List[str]): + """Prints a news article list + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-p", "20", "-n", "5"] + """ + + parser = argparse.ArgumentParser( + add_help=False, + prog="up_articles", + description="""Latest articles. [Source: Seeking Alpha]""", + ) + + parser.add_argument( + "-p", + "--pages", + action="store", + dest="n_pages", + type=check_positive, + default=1, + help="Number of pages from Seeking Alpha website.", + ) + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + articles = seeking_alpha_model.get_article_list(ns_parser.n_pages) + for article in articles: + print(article['id'], '-', article['title'], '(', article['url'], ')') + + +def news_article_view(other_args: List[str]): + """Prints a news article + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-i", "3681944"] + """ + + parser = argparse.ArgumentParser( + add_help=False, + prog="up_article", + description="""News article. [Source: Seeking Alpha]""", + ) + + parser.add_argument( + "-i", + "--id", + action="store", + dest="n_id", + type=check_positive, + default=-1, + help="Article number found on Seeking Alpha website.", + ) + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + if ns_parser.n_id == -1: + return + + article = seeking_alpha_model.get_article_data(ns_parser.n_id) + print(article['date'], article['title']) + print(article['content']) From 4f9669684b1053462dcf4de47f237ea64f354078 Mon Sep 17 00:00:00 2001 From: didier Date: Thu, 15 Apr 2021 22:12:45 +0100 Subject: [PATCH 038/112] Improve portfolio optimization user experience - pedantic stuff --- .../optimizer_helper.py | 57 ++++++----- .../portfolio_optimization/optimizer_view.py | 23 ++--- .../portfolio_optimization/po_controller.py | 95 +++++++++++-------- 3 files changed, 98 insertions(+), 77 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py index 3ca81f396a2..24db45eed91 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py @@ -27,7 +27,7 @@ title_maps = { def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFrame: - """ + """Get adjusted closing price for each stock in the list Parameters ---------- @@ -51,8 +51,8 @@ def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFra def prepare_efficient_frontier(stock_prices: pd.DataFrame): - """ - Take in a dataframe of prices and return an efficient frontier object + """Take in a dataframe of prices and return an efficient frontier object + Parameters ---------- stock_prices : DataFrame @@ -82,11 +82,11 @@ def display_weights(weights: dict): weight_df = pd.DataFrame.from_dict(data=weights, orient="index", columns=["value"]) 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] + lambda s: " " + s[:4] if s.find(".") == 1 else "" + s[:5] ) + " %" - print(pd.DataFrame(weight_df["weight"])) + print(pd.DataFrame(weight_df["weight"]).to_string(header=False)) else: - print(weight_df) + print(weight_df.to_string(header=False)) def my_autopct(x): @@ -107,7 +107,6 @@ def pie_chart_weights(weights: dict, optimizer: str, value: float): optimzer: str Optimization technique used for title """ - plt.close("all") if not weights: return @@ -122,31 +121,28 @@ def pie_chart_weights(weights: dict, optimizer: str, value: float): 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) - ] plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI) + if math.isclose(sum(sizes), 1, rel_tol=0.1): wedges, _, autotexts = plt.pie( sizes, labels=stocks, autopct=my_autopct, textprops=dict(color="k"), - explode=[s / (5 * total_size) for s in sizes], + wedgeprops={"linewidth": 3.0, "edgecolor": "white"}, normalize=True, - shadow=True, ) + plt.setp(autotexts, color="white", fontweight="bold") else: wedges, _, autotexts = plt.pie( sizes, labels=stocks, autopct="", textprops=dict(color="k"), - explode=[s / (5 * total_size) for s in sizes], + wedgeprops={"linewidth": 3.0, "edgecolor": "white"}, normalize=True, - shadow=True, ) + plt.setp(autotexts, color="white", fontweight="bold") for i, a in enumerate(autotexts): if sizes[i] / total_size > 0.05: a.set_text(f"{sizes[i]:.2f}") @@ -155,24 +151,41 @@ def pie_chart_weights(weights: dict, optimizer: str, value: float): plt.axis("equal") - plt.legend( + leg1 = plt.legend( wedges, - leg_labels, - title="Stocks", - loc="center left", - bbox_to_anchor=(0.85, 0, 0.5, 1), + [str(s) for s in stocks], + title=" Ticker", + loc="upper left", + bbox_to_anchor=(0.80, 0, 0.5, 1), + frameon=False, ) + leg2 = plt.legend( + wedges, + [ + f"{' ' if ((100*s/total_size) < 10) else ''}{100*s/total_size:.2f}%" + for s in sizes + ], + title=" ", + loc="upper left", + handlelength=0, + bbox_to_anchor=(0.91, 0, 0.5, 1), + frameon=False, + ) + plt.gca().add_artist(leg1) + plt.gca().add_artist(leg2) plt.setp(autotexts, size=8, weight="bold") if optimizer in ["eff_ret", "eff_risk"]: - plt.title(title_maps[optimizer].format(100 * value)) + plt.gca().set_title(title_maps[optimizer].format(100 * value), pad=20) else: - plt.title(title_maps[optimizer]) + plt.gca().set_title(title_maps[optimizer], pad=20) if gtff.USE_ION: plt.ion() + plt.tight_layout() + plt.show() print("") diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py index e9d74a76138..6e779b0669a 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -76,14 +76,15 @@ def equal_weight(stocks: List[str], other_args: List[str]): ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return values = {} - n_stocks = len(stocks) for stock in stocks: - values[stock] = ns_parser.value * round(1 / n_stocks, 5) + values[stock] = ns_parser.value * round(1 / len(stocks), 5) if ns_parser.pie: pie_chart_weights(values, "equal", 0) - if n_stocks >= 1: - print("Equal Weight Portfolio: ") + else: display_weights(values) print("") @@ -137,6 +138,9 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return for stock in stocks: stock_prop = yf.Ticker(stock).info[property_type] if stock_prop is None: @@ -148,14 +152,7 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s if ns_parser.pie: pie_chart_weights(weights, property_type, 0) - - if property_type == "marketCap": - print("Market Cap Weighted Portfolio: ") - - elif property_type == "dividendYield": - print("Dividend Yield Weighted Portfolio: ") - - if len(stocks) >= 1: + else: display_weights(weights) print("") @@ -296,7 +293,7 @@ def show_ef(stocks: List[str], other_args: List[str]): if not ns_parser: return if len(stocks) < 2: - print("Please have at least 2 loaded tickers to calculate weights.\n") + print("Please have at least 2 loaded tickers to calculate weights.") return stock_prices = process_stocks(stocks, ns_parser.period) diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index df2f8a55fa6..d4d2960adee 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -9,7 +9,7 @@ from prompt_toolkit.completion import NestedCompleter from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import get_flair, parse_known_args_and_warn from gamestonk_terminal.menu import session -from gamestonk_terminal.portfolio_optimization import optimizer_view as po_api +from gamestonk_terminal.portfolio_optimization import optimizer_view class PortfolioOptimization: @@ -20,14 +20,14 @@ class PortfolioOptimization: "quit", "select", "add", - "equal_weight", - "mkt_cap", - "div_yield", - "max_sharpe", - "min_vol", - "eff_risk", - "eff_ret", - "show_ef", + "equal", + "mktcap", + "dividend", + "maxsharpe", + "minvol", + "maxret", + "minrisk", + "ef", "yolo", ] @@ -52,21 +52,19 @@ class PortfolioOptimization: print(" quit quit to abandon program") print(f"\nCurrent Tickers: {('None', ', '.join(tickers))[bool(tickers)]}") print("") - print(" add add ticker to optimize") - print(" select overwrite current tickers with new tickers") + print(" add add ticker to optimize") + print(" select overwrite current tickers with new tickers") print("") print("Optimization:") + print(" equal equally weighted") + print(" mktcap market cap weighted") + print(" dividend dividend weighted") + print(" maxsharpe maximum sharpe ratio") + print(" minvol minimum volatility") + print(" maxret maximizes returns at given risk") + print(" minrisk minimizes risk at given return") + print(" ef show the efficient frontier") print("") - 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") def switch(self, an_input: str): """Process and dispatch input @@ -97,36 +95,45 @@ class PortfolioOptimization: return True def call_add(self, other_args: List[str]): + """Process add command""" self.add_stocks(self, other_args) def call_select(self, other_args: List[str]): + """Process select command""" self.tickers = [] self.add_stocks(self, other_args) - def call_equal_weight(self, other_args: List[str]): - po_api.equal_weight(self.tickers, other_args) + def call_equal(self, other_args: List[str]): + """Process equal command""" + optimizer_view.equal_weight(self.tickers, other_args) - def call_mkt_cap(self, other_args: List[str]): - po_api.property_weighting(self.tickers, "marketCap", other_args) + def call_mktcap(self, other_args: List[str]): + """Process mktcap command""" + optimizer_view.property_weighting(self.tickers, "marketCap", other_args) - def call_div_yield(self, other_args: List[str]): - po_api.property_weighting(self.tickers, "dividendYield", other_args) + def call_dividend(self, other_args: List[str]): + """Process dividend command""" + optimizer_view.property_weighting(self.tickers, "dividendYield", other_args) - def call_max_sharpe(self, other_args: List[str]): - po_api.ef_portfolio(self.tickers, "max_sharpe", other_args) + def call_maxsharpe(self, other_args: List[str]): + """Process maxsharpe command""" + optimizer_view.ef_portfolio(self.tickers, "max_sharpe", other_args) - def call_min_vol(self, other_args: List[str]): - po_api.ef_portfolio(self.tickers, "min_volatility", other_args) + def call_minvol(self, other_args: List[str]): + """Process minvol command""" + optimizer_view.ef_portfolio(self.tickers, "min_volatility", other_args) - def call_eff_risk(self, other_args: List[str]): - po_api.ef_portfolio(self.tickers, "eff_risk", other_args) + def call_maxret(self, other_args: List[str]): + """Process maxret command""" + optimizer_view.ef_portfolio(self.tickers, "eff_risk", other_args) - def call_eff_ret(self, other_args: List[str]): - po_api.ef_portfolio(self.tickers, "eff_ret", other_args) + def call_minrisk(self, other_args: List[str]): + """Process minrisk command""" + optimizer_view.ef_portfolio(self.tickers, "eff_ret", other_args) - def call_show_ef(self, other_args): - po_api.show_ef(self.tickers, other_args) - print("") + def call_ef(self, other_args): + """Process ef command""" + optimizer_view.show_ef(self.tickers, other_args) def call_yolo(self, _): # Easter egg :) @@ -162,12 +169,14 @@ class PortfolioOptimization: tickers = set(self.tickers) for ticker in ns_parser.add_tickers: tickers.add(ticker) + + if self.tickers: + print( + f"\nCurrent Tickers: {('None', ', '.join(tickers))[bool(tickers)]}" + ) + self.tickers = list(tickers) - print( - f"\nCurrent Tickers: {('None', ', '.join(self.tickers))[bool(self.tickers)]}" - ) - print("") except Exception as e: print(e) @@ -196,6 +205,8 @@ def menu(tickers: List[str]): an_input = input(f"{get_flair()} (po)> ") try: + plt.close("all") + process_input = po_controller.switch(an_input) if process_input is not None: From 57d56b8b1e2903fada239270b2c3cb59f5ea3e5d Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 00:38:14 +0100 Subject: [PATCH 039/112] some more minor refactoring cuz im picky --- gamestonk_terminal/helper_funcs.py | 2 +- .../optimizer_helper.py | 176 +++++++----- .../portfolio_optimization/optimizer_view.py | 256 +++++++++++++----- .../portfolio_optimization/po_controller.py | 55 ++-- 4 files changed, 335 insertions(+), 154 deletions(-) diff --git a/gamestonk_terminal/helper_funcs.py b/gamestonk_terminal/helper_funcs.py index 11a3ffbe196..a2e00fd3d35 100644 --- a/gamestonk_terminal/helper_funcs.py +++ b/gamestonk_terminal/helper_funcs.py @@ -347,7 +347,7 @@ def patch_pandas_text_adjustment(): def parse_known_args_and_warn(parser, l_args): parser.add_argument( - "-h", "--help", action="store_true", dest="help", help="show this help message" + "-h", "--help", action="store_true", help="show this help message" ) (ns_parser, l_unknown_args) = parser.parse_known_args(l_args) diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py index 24db45eed91..19f9057bd76 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py @@ -15,15 +15,107 @@ from gamestonk_terminal.config_plot import PLOT_DPI from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import plot_autoscale, parse_known_args_and_warn -title_maps = { - "max_sharpe": "Maximum Sharpe Portfolio", - "min_volatility": "Minimum Volatility Portfolio", - "eff_risk": "Maximum Return Portfolio at Risk = {:1f} %", - "eff_ret": "Minimum Volatility Portfolio at Target Return = {:.1f} %", - "equal": "Equally Weighted Portfolio", - "marketCap": "MarketCap Weighted Portfolio", - "dividendYield": "Dividend Yield Weighted Portfolio", -} +l_valid_property_infos = [ + "previousClose", + "regularMarketOpen", + "twoHundredDayAverage", + "trailingAnnualDividendYield", + "payoutRatio", + "volume24Hr", + "regularMarketDayHigh", + "navPrice", + "averageDailyVolume10Day", + "totalAssets", + "regularMarketPreviousClose", + "fiftyDayAverage", + "trailingAnnualDividendRate", + "open", + "toCurrency", + "averageVolume10days", + "expireDate", + "yield", + "algorithm", + "dividendRate", + "exDividendDate", + "beta", + "circulatingSupply", + "regularMarketDayLow", + "priceHint", + "currency", + "trailingPE", + "regularMarketVolume", + "lastMarket", + "maxSupply", + "openInterest", + "marketCap", + "volumeAllCurrencies", + "strikePrice", + "averageVolume", + "priceToSalesTrailing12Months", + "dayLow", + "ask", + "ytdReturn", + "askSize", + "volume", + "fiftyTwoWeekHigh", + "forwardPE", + "fromCurrency", + "fiveYearAvgDividendYield", + "fiftyTwoWeekLow", + "bid", + "dividendYield", + "bidSize", + "dayHigh", + "annualHoldingsTurnover", + "enterpriseToRevenue", + "beta3Year", + "profitMargins", + "enterpriseToEbitda", + "52WeekChange", + "morningStarRiskRating", + "forwardEps", + "revenueQuarterlyGrowth", + "sharesOutstanding", + "fundInceptionDate", + "annualReportExpenseRatio", + "bookValue", + "sharesShort", + "sharesPercentSharesOut", + "fundFamily", + "lastFiscalYearEnd", + "heldPercentInstitutions", + "netIncomeToCommon", + "trailingEps", + "lastDividendValue", + "SandP52WeekChange", + "priceToBook", + "heldPercentInsiders", + "shortRatio", + "sharesShortPreviousMonthDate", + "floatShares", + "enterpriseValue", + "threeYearAverageReturn", + "lastSplitFactor", + "legalType", + "lastDividendDate", + "morningStarOverallRating", + "earningsQuarterlyGrowth", + "pegRatio", + "lastCapGain", + "shortPercentOfFloat", + "sharesShortPriorMonth", + "impliedSharesOutstanding", + "fiveYearAverageReturn", + "regularMarketPrice", +] + + +def check_valid_property_type(property: str) -> str: + """ Check that the property selected is valid """ + if property in l_valid_property_infos: + return property + else: + raise argparse.ArgumentTypeError(f"{property} is not a valid info") def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFrame: @@ -97,15 +189,15 @@ def my_autopct(x): return "" -def pie_chart_weights(weights: dict, optimizer: str, value: float): - """ - Show a pie chart of holdings +def pie_chart_weights(weights: dict, title_opt: str): + """Show a pie chart of holdings + Parameters ---------- weights: dict - weights to display. Keys are stocks. Values are either weights or values if -v specified - optimzer: str - Optimization technique used for title + Weights to display, where keys are tickers, and values are either weights or values if -v specified + title: str + Title to be used on the plot title """ if not weights: return @@ -176,10 +268,7 @@ def pie_chart_weights(weights: dict, optimizer: str, value: float): plt.setp(autotexts, size=8, weight="bold") - if optimizer in ["eff_ret", "eff_risk"]: - plt.gca().set_title(title_maps[optimizer].format(100 * value), pad=20) - else: - plt.gca().set_title(title_maps[optimizer], pad=20) + plt.gca().set_title(title_opt, pad=20) if gtff.USE_ION: plt.ion() @@ -188,52 +277,3 @@ def pie_chart_weights(weights: dict, optimizer: str, value: float): plt.show() print("") - - -def parse_from_port_type( - parser_in: argparse.ArgumentParser, port_type: str, other_args: List[str] -): - """ - - Parameters - ---------- - parser_in: ArgumentParser - parser to get data from - port_type: str - Type of optimization that will be done. One of ["max_sharpe","min_vol", "eff_risk", "eff_ret"] - other_args: List[str] - Arguments passed to function - Returns - ------- - - ns_parser: - Parsed arguments - """ - - if port_type in ["max_sharpe", "min_volatility"]: - ns_parser = parse_known_args_and_warn(parser_in, other_args) - if not ns_parser: - return None - return ns_parser - - elif port_type == "eff_risk": - parser_in.add_argument( - "-r", "--risk", type=float, dest="risk_level", default=0.1 - ) - ns_parser = parse_known_args_and_warn(parser_in, other_args) - if not ns_parser: - return None - return ns_parser - - elif port_type == "eff_ret": - parser_in.add_argument( - "-r", "--return", type=float, dest="target_return", default=0.1 - ) - - ns_parser = parse_known_args_and_warn(parser_in, other_args) - if not ns_parser: - return None - return ns_parser - else: - print("Incorrect portfolio optimizer type\n") - return None diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py index 6e779b0669a..4f7170e1fd8 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -16,7 +16,7 @@ from gamestonk_terminal.portfolio_optimization.optimizer_helper import ( prepare_efficient_frontier, pie_chart_weights, display_weights, - parse_from_port_type, + check_valid_property_type, ) from gamestonk_terminal.config_plot import PLOT_DPI from gamestonk_terminal import feature_flags as gtff @@ -35,6 +35,20 @@ period_choices = [ "max", ] +d_period = { + "1d": "[1 Day]", + "5d": "[5 Days]", + "1mo": "[1 Month]", + "3mo": "[3 Months]", + "6mo": "[6 Months]", + "1y": "[1 Year]", + "2y": "[2 Years]", + "5y": "[5 Years]", + "10y": "[10 Years]", + "ytd": "[Year-to-Date]", + "max": "[All-time]", +} + def equal_weight(stocks: List[str], other_args: List[str]): """ @@ -53,8 +67,8 @@ def equal_weight(stocks: List[str], other_args: List[str]): """ parser = argparse.ArgumentParser( add_help=False, - prog="equal_weight", - description="Return equally weighted portfolio holdings", + prog="equal", + description="Returns an equally weighted portfolio", ) parser.add_argument( @@ -83,7 +97,7 @@ def equal_weight(stocks: List[str], other_args: List[str]): for stock in stocks: values[stock] = ns_parser.value * round(1 / len(stocks), 5) if ns_parser.pie: - pie_chart_weights(values, "equal", 0) + pie_chart_weights(values, "Equally Weighted Portfolio") else: display_weights(values) print("") @@ -93,16 +107,15 @@ def equal_weight(stocks: List[str], other_args: List[str]): print("") -def property_weighting(stocks: List[str], property_type: str, other_args: List[str]): - """ - Property weighted portfolio where each weight is the relative fraction. +def property_weighting(stocks: List[str], other_args: List[str]): + """Weighted portfolio where each weight is the relative fraction of a specified info property. + Parameters ---------- stocks: List[str] List of tickers to be included in optimization - property_type: str - Property to weight by. Can be anything in yfinance.Ticker().info. Examples: - "marketCap", "dividendYield", etc + other_args : List[str] + Command line arguments to be processed with argparse Returns ------- @@ -111,10 +124,32 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s """ parser = argparse.ArgumentParser( add_help=False, - prog="market_cap_weighted", - description="Return portfolio weights/values that are weighted by marketcap", + prog="property", + description="Returns a portfolio that is weighted based on a selected property info", + ) + parser.add_argument( + "-p", + "--property", + required=True, + type=check_valid_property_type, + dest="property", + help="""Property info to weigh. Use one of: + previousClose, regularMarketOpen, twoHundredDayAverage, trailingAnnualDividendYield, + payoutRatio, volume24Hr, regularMarketDayHigh, navPrice, averageDailyVolume10Day, totalAssets, + regularMarketPreviousClose, fiftyDayAverage, trailingAnnualDividendRate, open, toCurrency, averageVolume10days, + expireDate, yield, algorithm, dividendRate, exDividendDate, beta, circulatingSupply, regularMarketDayLow, + priceHint, currency, trailingPE, regularMarketVolume, lastMarket, maxSupply, openInterest, marketCap, + volumeAllCurrencies, strikePrice, averageVolume, priceToSalesTrailing12Months, dayLow, ask, ytdReturn, askSize, + volume, fiftyTwoWeekHigh, forwardPE, fromCurrency, fiveYearAvgDividendYield, fiftyTwoWeekLow, bid, dividendYield, + bidSize, dayHigh, annualHoldingsTurnover, enterpriseToRevenue, beta3Year, profitMargins, enterpriseToEbitda, + 52WeekChange, morningStarRiskRating, forwardEps, revenueQuarterlyGrowth, sharesOutstanding, fundInceptionDate, + annualReportExpenseRatio, bookValue, sharesShort, sharesPercentSharesOut, fundFamily, lastFiscalYearEnd, + heldPercentInstitutions, netIncomeToCommon, trailingEps, lastDividendValue, SandP52WeekChange, priceToBook, + heldPercentInsiders, shortRatio, sharesShortPreviousMonthDate, floatShares, enterpriseValue, + threeYearAverageReturn, lastSplitFactor, legalType, lastDividendDate, morningStarOverallRating, + earningsQuarterlyGrowth, pegRatio, lastCapGain, shortPercentOfFloat, sharesShortPriorMonth, + impliedSharesOutstanding, fiveYearAverageReturn, and regularMarketPrice.""", ) - parser.add_argument( "-v", "--value", @@ -130,19 +165,24 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s default=False, help="Display a pie chart for weights", ) - weights = {} - prop = {} - prop_sum = 0 try: + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-p") + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return if len(stocks) < 2: print("Please have at least 2 loaded tickers to calculate weights.\n") return + + weights = {} + prop = {} + prop_sum = 0 for stock in stocks: - stock_prop = yf.Ticker(stock).info[property_type] + stock_prop = yf.Ticker(stock).info[ns_parser.property] if stock_prop is None: stock_prop = 0 prop[stock] = stock_prop @@ -151,7 +191,9 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s weights[k] = round(v / prop_sum, 5) * ns_parser.value if ns_parser.pie: - pie_chart_weights(weights, property_type, 0) + pie_chart_weights( + weights, "Weighted Portfolio based on " + ns_parser.property + ) else: display_weights(weights) print("") @@ -165,20 +207,37 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): """ Return a portfolio based on condition in port_type Defaults to 3m of historical data + Parameters ---------- stocks: List[str] List of the stocks to be included in the weights port_type: str - Method to be used on ef object (example: max_sharpe, min_volatility) + Method to be used on ef object (example: max_sharpe, min_volatility, eff_risk, eff_ret) + Returns ------- weights: dict Dictionary of weights where keys are the tickers. """ - parser = argparse.ArgumentParser(add_help=False, prog=port_type) + d_descriptions = { + "max_sharpe": """Maximise the Sharpe Ratio. The result is also referred to as the tangency portfolio, + as it is the portfolio for which the capital market line is tangent to the efficient frontier. + This is a convex optimization problem after making a certain variable substitution. See + Cornuejols and Tutuncu (2006) for more.""", + "min_volatility": "optimizes for minimum volatility", + "max_quadratic_utility": "maximises the quadratic utility, given some risk aversion", + "efficient_risk": """Maximise return for a target risk. The resulting portfolio will have + a volatility less than the target (but not guaranteed to be equal).""", + "efficient_return": "Calculate the 'Markowitz portfolio', minimising volatility for a given target return.", + "max_quadratic_utility": "Maximise the given quadratic utility, i.e: max_w w^T \mu - \frac \delta 2 w^T \Sigma w", + } + + parser = argparse.ArgumentParser( + add_help=False, prog=port_type, description=d_descriptions[port_type] + ) parser.add_argument( "-p", "--period", @@ -187,7 +246,6 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): help="period to get yfinance data from", choices=period_choices, ) - parser.add_argument( "-v", "--value", @@ -204,68 +262,144 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): help="Display a pie chart for weights", ) + if port_type == "max_sharpe": + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-r") + parser.add_argument( + "-r", + "--risk_free_rate", + type=float, + dest="risk_free_rate", + default=0.02, + help="""Risk-free rate of borrowing/lending. The period of the risk-free rate + should correspond to the frequency of expected returns.""", + ) + + elif port_type == "efficient_risk": + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-t") + parser.add_argument( + "-t", + "--target_volatility", + type=float, + dest="target_volatility", + default=0.1, + help="The desired maximum volatility of the resulting portfolio", + ) + parser.add_argument( + "-n", + "--market_neutral", + action="store_true", + default=False, + dest="market_neutral", + help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False. + Requires negative lower weight bound.""", + ) + + elif port_type == "efficient_return": + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-t") + parser.add_argument( + "-t", + "--target_return", + type=float, + dest="target_return", + default=0.1, + help="the desired return of the resulting portfolio", + ) + parser.add_argument( + "-n", + "--market_neutral", + action="store_true", + default=False, + dest="market_neutral", + help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False. + Requires negative lower weight bound.""", + ) + + elif port_type == "max_quadratic_utility": + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-r") + parser.add_argument( + "-r", + "--risk_aversion", + type=float, + dest="risk_aversion", + default=1, + help="risk aversion parameter", + ) + parser.add_argument( + "-n", + "--market_neutral", + action="store_true", + default=False, + dest="market_neutral", + help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False. + Requires negative lower weight bound.""", + ) + try: - ns_parser = parse_from_port_type(parser, port_type, other_args) + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: - return + return None if len(stocks) < 2: print("Please have at least 2 loaded tickers to calculate weights.\n") return + period = ns_parser.period stock_prices = process_stocks(stocks, period) ef = prepare_efficient_frontier(stock_prices) - if port_type == "max_sharpe": + sp = d_period[ns_parser.period] - ef_sharpe = dict(ef.max_sharpe()) - weights = { - key: ns_parser.value * round(value, 5) - for key, value in ef_sharpe.items() - } - val = 0 - print("Weights that maximize Sharpe Ratio:") + if port_type == "max_sharpe": + ef_opt = dict(ef.max_sharpe(ns_parser.risk_free_rate)) + s_title = f"{sp} Weights that maximize Sharpe ration with risk free level of {ns_parser.risk_free_rate}" elif port_type == "min_volatility": + ef_opt = dict(ef.min_volatility()) + s_title = f"{sp} Weights that minimize volatility" - ef_min_vol = dict(ef.min_volatility()) - weights = { - key: ns_parser.value * round(value, 5) - for key, value in ef_min_vol.items() - } - val = 0 - print("Weights that minimize volatility") + elif port_type == "efficient_risk": + ef_opt = dict( + ef.efficient_risk(ns_parser.target_volatility, ns_parser.market_neutral) + ) + s_title = f"{sp} Weights that maximize return with a maximum volatility of {ns_parser.target_volatility}" - elif port_type == "eff_risk": + elif port_type == "efficient_return": + ef_opt = dict( + ef.efficient_return(ns_parser.target_return, ns_parser.market_neutral) + ) + s_title = f"{sp} Weights that minimise volatility for a given target return of {ns_parser.target_return}" - ef_eff_risk = dict(ef.efficient_risk(ns_parser.risk_level)) - weights = { - key: ns_parser.value * round(value, 5) - for key, value in ef_eff_risk.items() - } - val = ns_parser.risk_level - print(f"Weights for maximizing returns at risk = {100*val:.1f} %") - - elif port_type == "eff_ret": - - ef_eff_risk = dict(ef.efficient_return(ns_parser.target_return)) - weights = { - key: ns_parser.value * round(value, 5) - for key, value in ef_eff_risk.items() - } - val = ns_parser.target_return - print(f"Weights for minimizing risk at target return = {100*val:.1f} %") + elif port_type == "max_quadratic_utility": + ef_opt = dict( + ef.max_quadratic_utility( + ns_parser.risk_aversion, ns_parser.market_neutral + ) + ) + s_title = f"{sp} Weights that maximise the quadratic utility with risk aversion of {ns_parser.risk_aversion}" else: raise ValueError("EF Method not found") - if ns_parser.pie: - pie_chart_weights(weights, port_type, val) + weights = { + key: ns_parser.value * round(value, 5) for key, value in ef_opt.items() + } + + if ns_parser.pie: + pie_chart_weights(weights, s_title) + else: + print(s_title) + display_weights(weights) + print("") - print("") ef.portfolio_performance(verbose=True) print("") - display_weights(weights) - print("") except Exception as e: print(e) diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index d4d2960adee..24a93bcdd5e 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -21,12 +21,12 @@ class PortfolioOptimization: "select", "add", "equal", - "mktcap", - "dividend", + "property", "maxsharpe", "minvol", - "maxret", - "minrisk", + "effret", + "effrisk", + "maxquadutil", "ef", "yolo", ] @@ -57,12 +57,20 @@ class PortfolioOptimization: print("") print("Optimization:") print(" equal equally weighted") - print(" mktcap market cap weighted") - print(" dividend dividend weighted") - print(" maxsharpe maximum sharpe ratio") - print(" minvol minimum volatility") - print(" maxret maximizes returns at given risk") - print(" minrisk minimizes risk at given return") + print( + " property weight according to selected info property (e.g. marketCap)" + ) + print("") + print( + " maxsharpe optimizes for maximal Sharpe ratio (a.k.a the tangency portfolio)" + ) + print(" minvol optimizes for minimum volatility") + print( + " maxquadutil maximises the quadratic utility, given some risk aversion" + ) + print(" effret maximises return for a given target risk") + print(" effrisk minimises risk for a given target return") + print("") print(" ef show the efficient frontier") print("") @@ -107,13 +115,9 @@ class PortfolioOptimization: """Process equal command""" optimizer_view.equal_weight(self.tickers, other_args) - def call_mktcap(self, other_args: List[str]): - """Process mktcap command""" - optimizer_view.property_weighting(self.tickers, "marketCap", other_args) - - def call_dividend(self, other_args: List[str]): - """Process dividend command""" - optimizer_view.property_weighting(self.tickers, "dividendYield", other_args) + def call_property(self, other_args: List[str]): + """Process property command""" + optimizer_view.property_weighting(self.tickers, other_args) def call_maxsharpe(self, other_args: List[str]): """Process maxsharpe command""" @@ -123,13 +127,17 @@ class PortfolioOptimization: """Process minvol command""" optimizer_view.ef_portfolio(self.tickers, "min_volatility", other_args) - def call_maxret(self, other_args: List[str]): - """Process maxret command""" - optimizer_view.ef_portfolio(self.tickers, "eff_risk", other_args) + def call_maxquadutil(self, other_args: List[str]): + """Process maxquadutil command""" + optimizer_view.ef_portfolio(self.tickers, "max_quadratic_utility", other_args) - def call_minrisk(self, other_args: List[str]): - """Process minrisk command""" - optimizer_view.ef_portfolio(self.tickers, "eff_ret", other_args) + def call_effrisk(self, other_args: List[str]): + """Process effrisk command""" + optimizer_view.ef_portfolio(self.tickers, "efficient_risk", other_args) + + def call_effret(self, other_args: List[str]): + """Process effret command""" + optimizer_view.ef_portfolio(self.tickers, "efficient_return", other_args) def call_ef(self, other_args): """Process ef command""" @@ -158,7 +166,6 @@ class PortfolioOptimization: help="add tickers to optimzation.", ) try: - if other_args: if "-" not in other_args[0]: other_args.insert(0, "-t") From b19f0740b57bedd1eb9e172392ef95194910ee8c Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 00:59:42 +0100 Subject: [PATCH 040/112] more refactoring and stuff --- .../comparison_analysis/ca_controller.py | 2 +- gamestonk_terminal/screener/finviz_view.py | 8 ++++-- .../screener/screener_controller.py | 28 +++++++++++-------- .../screener/yahoo_finance_view.py | 12 ++++++-- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py index f999ed092af..e0a339202c2 100644 --- a/gamestonk_terminal/comparison_analysis/ca_controller.py +++ b/gamestonk_terminal/comparison_analysis/ca_controller.py @@ -320,7 +320,7 @@ class ComparisonAnalysisController: finviz_compare_view.screener(other_args, "technical", self.ticker, self.similar) def call_po(self, _): - """Open Portfolio Optimization menu with ticker and similar""" + """Call the portfolio optimization menu with selected tickers""" return po_controller.menu([self.ticker] + self.similar) diff --git a/gamestonk_terminal/screener/finviz_view.py b/gamestonk_terminal/screener/finviz_view.py index 4ca3bb96857..3c331be9649 100644 --- a/gamestonk_terminal/screener/finviz_view.py +++ b/gamestonk_terminal/screener/finviz_view.py @@ -214,12 +214,16 @@ def screener(other_args: List[str], loaded_preset: str, data_type: str): if isinstance(df_screen, pd.DataFrame): print(df_screen.to_string()) - print("") + print("") + return list(df_screen["Ticker"].values) + else: + print("") + return [] except Exception as e: print(e) print("") - return + return [] def view_signals(other_args: List[str]): diff --git a/gamestonk_terminal/screener/screener_controller.py b/gamestonk_terminal/screener/screener_controller.py index 0fe364750b0..7edc438c501 100644 --- a/gamestonk_terminal/screener/screener_controller.py +++ b/gamestonk_terminal/screener/screener_controller.py @@ -41,6 +41,7 @@ class ScreenerController: def __init__(self): """Constructor""" self.preset = "template" + self.screen_tickers = [] self.scr_parser = argparse.ArgumentParser(add_help=False, prog="scr") self.scr_parser.add_argument( "cmd", @@ -71,8 +72,11 @@ class ScreenerController: print("") print(" signals view filter signals (e.g. -s top_gainers)") print("") - print(" > po go to the portfolio optimization menu") - print("") + if self.screen_tickers: + print(f"Last screened tickers: {', '.join(self.screen_tickers)}") + print("") + print(" > po portfolio optimization for last screened tickers") + print("") @staticmethod def view_available_presets(other_args: List[str]): @@ -191,39 +195,41 @@ class ScreenerController: def call_historical(self, other_args: List[str]): """Process historical command""" - yahoo_finance_view.historical(other_args, self.preset) + self.screen_tickers = yahoo_finance_view.historical(other_args, self.preset) def call_overview(self, other_args: List[str]): """Process overview command""" - finviz_view.screener(other_args, self.preset, "overview") + self.screen_tickers = finviz_view.screener(other_args, self.preset, "overview") def call_valuation(self, other_args: List[str]): """Process valuation command""" - finviz_view.screener(other_args, self.preset, "valuation") + self.screen_tickers = finviz_view.screener(other_args, self.preset, "valuation") def call_financial(self, other_args: List[str]): """Process financial command""" - finviz_view.screener(other_args, self.preset, "financial") + self.screen_tickers = finviz_view.screener(other_args, self.preset, "financial") def call_ownership(self, other_args: List[str]): """Process ownership command""" - finviz_view.screener(other_args, self.preset, "ownership") + self.screen_tickers = finviz_view.screener(other_args, self.preset, "ownership") def call_performance(self, other_args: List[str]): """Process performance command""" - finviz_view.screener(other_args, self.preset, "performance") + self.screen_tickers = finviz_view.screener( + other_args, self.preset, "performance" + ) def call_technical(self, other_args: List[str]): """Process technical command""" - finviz_view.screener(other_args, self.preset, "technical") + self.screen_tickers = finviz_view.screener(other_args, self.preset, "technical") def call_signals(self, other_args: List[str]): """Process signals command""" 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) + """Call the portfolio optimization menu with selected tickers""" + return po_controller.menu(self.screen_tickers) def menu(): diff --git a/gamestonk_terminal/screener/yahoo_finance_view.py b/gamestonk_terminal/screener/yahoo_finance_view.py index 6ad1c6e93bb..2a05ddbeec1 100644 --- a/gamestonk_terminal/screener/yahoo_finance_view.py +++ b/gamestonk_terminal/screener/yahoo_finance_view.py @@ -1,5 +1,6 @@ import argparse from typing import List +import random from pandas.plotting import register_matplotlib_converters import matplotlib.pyplot as plt import datetime @@ -119,10 +120,12 @@ def historical(other_args: List[str], preset_loaded: str): if len(l_stocks) > 10: print( - "We limit stocks shown to 10, as after this the plot becomes too noisy. " - "Also, we ran out of different colors." + "\nThe limit of stocks to compare with are 10. Hence, 10 random similar stocks will be displayed.", + "\nThe selected list will be:", ) - l_stocks = l_stocks[:10] + random.shuffle(l_stocks) + l_stocks = sorted(l_stocks[:10]) + print(", ".join(l_stocks)) while l_stocks: l_parsed_stocks = list() @@ -179,8 +182,11 @@ def historical(other_args: List[str], preset_loaded: str): plt.show() print("") + return l_parsed_stocks except SystemExit: print("Similar companies need to be provided", "\n") + return [] except Exception as e: print(e, "\n") + return [] From 63e75010666e21dfc1523046f3a10e5cab3059d3 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 01:07:13 +0100 Subject: [PATCH 041/112] Update README.md --- gamestonk_terminal/README.md | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 2b6d79c7b2e..e616a2ae873 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -9,6 +9,7 @@ * [Due Diligence](#Due-Diligence-) * [Prediction Techniques](#Prediction-Techniques-) * [Portfolio Analysis](#Portfolio-Analysis-) +* [Portfolio Optimization](#Portfolio-Optimization-) * [Cryptocurrencies](#Cryptocurrencies-) * [Comparison Analysis](#Comparison-Analysis-) * [Exploratory Data Analysis](#Exploratory-Data-Analysis-) @@ -242,16 +243,19 @@ Command|Description|Brokers   ## Portfolio Optimization [»](portfolio_optimization/README.md) -command|desciription + +Command|Description ------|------ -`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 +`add`| add ticker to optimize +`select`| overwrite current tickers with new tickers +`equal`| equally weighted +`property`| weight according to selected info property (e.g. marketCap) +`maxsharpe`| optimizes for maximal Sharpe ratio (a.k.a the tangency portfolio) +`minvol`| optimizes for minimum volatility +`maxquadutil`| maximises the quadratic utility, given some risk aversion +`effret`| maximises return for a given target risk +`effrisk`| minimises risk for a given target return +`ef`| show the efficient frontier   @@ -331,7 +335,9 @@ Command|Description Command|Description|Source ------ | --------|---- `exp` | see/set expiry date -`volume` | plot options trading volume / open interest |[Yahoo Finance](https://finance.yahoo.com/) +`volume` | volume + open interest options trading plot |[Yahoo Finance](https://finance.yahoo.com/) +`vcalls` | calls volume + open interest plot |[Yahoo Finance](https://finance.yahoo.com/) +`vputs` | puts volume + open interest plot |[Yahoo Finance](https://finance.yahoo.com/)   @@ -351,4 +357,4 @@ technical |technical (e.g. Beta, SMA50, 52W Low, RSI, Change) signals |view filter signals (e.g. -s top_gainers) -  \ No newline at end of file +  From 1c04f02da4b0e295fc4d7483eb884d72c0c896ee Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 01:10:56 +0100 Subject: [PATCH 042/112] fix linting --- gamestonk_terminal/portfolio_optimization/optimizer_helper.py | 2 +- gamestonk_terminal/portfolio_optimization/optimizer_view.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py index 19f9057bd76..f88ad80463c 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py @@ -13,7 +13,7 @@ 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, parse_known_args_and_warn +from gamestonk_terminal.helper_funcs import plot_autoscale l_valid_property_infos = [ "previousClose", diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py index 4f7170e1fd8..92ee5417fb9 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -232,7 +232,6 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): "efficient_risk": """Maximise return for a target risk. The resulting portfolio will have a volatility less than the target (but not guaranteed to be equal).""", "efficient_return": "Calculate the 'Markowitz portfolio', minimising volatility for a given target return.", - "max_quadratic_utility": "Maximise the given quadratic utility, i.e: max_w w^T \mu - \frac \delta 2 w^T \Sigma w", } parser = argparse.ArgumentParser( From 446de338b05113ac401900d979f39a8816dfacf4 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 01:31:25 +0100 Subject: [PATCH 043/112] Update README.md --- .../portfolio_optimization/README.md | 95 +++++++++++++------ 1 file changed, 68 insertions(+), 27 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md index bf96cf77629..c79fd3c9023 100644 --- a/gamestonk_terminal/portfolio_optimization/README.md +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -1,35 +1,76 @@ # Portfolio Optimization -This menu aims to discover optimal portfolios for selected stocks. +This menu aims to optimize a portfolio of pre-loaded stocks, and the usage of the following commands along with an example will be exploited below. -Currently we support two methods: +* [add](#add) + * add ticker to optimize +* [select](#select) + * overwrite current tickers with new tickers +* [equal](#equal) + * equally weighted +* [property](#property) + * weight according to selected info property (e.g. marketCap) +* [maxsharpe](#maxsharpe) + * optimizes for maximal Sharpe ratio (a.k.a the tangency portfolio) +* [minvol](#minvol) + * optimizes for minimum volatility +* [maxquadutil](#maxquadutil) + * maximises the quadratic utility, given some risk aversion +* [effret](#effret) + * maximises return for a given target risk +* [effrisk](#effrisk) + * minimises risk for a given target return +* [ef](#ef) + * show the efficient frontier -* [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 selected tickers to the menu to be considered +## add + +```text +add [-t ADD_TICKERS] +``` + +Add/Select tickers for portfolio to be optimized. + +* -t : tickers to be used in the portfolio to optimize +* + +## select + +```text +select [-t ADD_TICKERS] +``` + +Add/Select tickers for portfolio to be optimized. + +* -t : tickers to be used in the portfolio to optimize + + +## equal + +```text +equal [-v VALUE] [--pie] +``` + +Returns an equally weighted portfolio + +* -v : Amount to allocate to portfolio. +* --pie : Display a pie chart for weights + + +## property + +```text +property [-p PROPERTY] [-v VALUE] [--pie] +``` + +Returns a portfolio that is weighted based on a selected property info + +* -p : Property info to weigh. Use one of: previousClose, regularMarketOpen, twoHundredDayAverage, trailingAnnualDividendYield, payoutRatio, volume24Hr, regularMarketDayHigh, navPrice, averageDailyVolume10Day, totalAssets, regularMarketPreviousClose, fiftyDayAverage, trailingAnnualDividendRate, open, toCurrency, averageVolume10days, expireDate, yield, algorithm, dividendRate, exDividendDate, beta, circulatingSupply, regularMarketDayLow, priceHint, currency, trailingPE, regularMarketVolume, lastMarket, maxSupply, openInterest, marketCap, volumeAllCurrencies, strikePrice, averageVolume, priceToSalesTrailing12Months, dayLow, ask, ytdReturn, askSize, volume, fiftyTwoWeekHigh, forwardPE, fromCurrency, fiveYearAvgDividendYield, fiftyTwoWeekLow, bid, dividendYield, bidSize, dayHigh, annualHoldingsTurnover, enterpriseToRevenue, beta3Year, profitMargins, enterpriseToEbitda, 52WeekChange, morningStarRiskRating, forwardEps, revenueQuarterlyGrowth, sharesOutstanding, fundInceptionDate, annualReportExpenseRatio, bookValue, sharesShort, sharesPercentSharesOut, fundFamily, lastFiscalYearEnd, heldPercentInstitutions, netIncomeToCommon, trailingEps, lastDividendValue, SandP52WeekChange, priceToBook, heldPercentInsiders, shortRatio, sharesShortPreviousMonthDate, floatShares, enterpriseValue, threeYearAverageReturn, lastSplitFactor, legalType, lastDividendDate, morningStarOverallRating, earningsQuarterlyGrowth, pegRatio, lastCapGain, shortPercentOfFloat, sharesShortPriorMonth, impliedSharesOutstanding, fiveYearAverageReturn, and regularMarketPrice. +* -v : Amount to allocate to portfolio. +* --pie : Display a pie chart for weights + -```` -usage: add ticker1,ticker2,ticker3,... -```` -###select -Clears current list and loads selected ticker -```` -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. @@ -166,4 +207,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 omitted from the chart. \ No newline at end of file +Note that since `AAPL` had zero allocation, it was omitted from the chart. From a21195438b4370578b688d7774706e9587801504 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 02:01:17 +0100 Subject: [PATCH 044/112] Update README.md --- .../portfolio_optimization/README.md | 202 +++++++++--------- 1 file changed, 97 insertions(+), 105 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md index c79fd3c9023..2825bf9311b 100644 --- a/gamestonk_terminal/portfolio_optimization/README.md +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -10,6 +10,9 @@ This menu aims to optimize a portfolio of pre-loaded stocks, and the usage of th * equally weighted * [property](#property) * weight according to selected info property (e.g. marketCap) + +[Mean Variance Optimization](#Mean_Variance_Optimization) + * [maxsharpe](#maxsharpe) * optimizes for maximal Sharpe ratio (a.k.a the tangency portfolio) * [minvol](#minvol) @@ -24,7 +27,7 @@ This menu aims to optimize a portfolio of pre-loaded stocks, and the usage of th * show the efficient frontier -## add +### add ```text add [-t ADD_TICKERS] @@ -32,10 +35,10 @@ add [-t ADD_TICKERS] Add/Select tickers for portfolio to be optimized. -* -t : tickers to be used in the portfolio to optimize +* -t : Tickers to be used in the portfolio to optimize * -## select +### select ```text select [-t ADD_TICKERS] @@ -43,10 +46,10 @@ select [-t ADD_TICKERS] Add/Select tickers for portfolio to be optimized. -* -t : tickers to be used in the portfolio to optimize +* -t : Tickers to be used in the portfolio to optimize -## equal +### equal ```text equal [-v VALUE] [--pie] @@ -54,11 +57,11 @@ equal [-v VALUE] [--pie] Returns an equally weighted portfolio -* -v : Amount to allocate to portfolio. -* --pie : Display a pie chart for weights +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights -## property +### property ```text property [-p PROPERTY] [-v VALUE] [--pie] @@ -67,122 +70,111 @@ property [-p PROPERTY] [-v VALUE] [--pie] Returns a portfolio that is weighted based on a selected property info * -p : Property info to weigh. Use one of: previousClose, regularMarketOpen, twoHundredDayAverage, trailingAnnualDividendYield, payoutRatio, volume24Hr, regularMarketDayHigh, navPrice, averageDailyVolume10Day, totalAssets, regularMarketPreviousClose, fiftyDayAverage, trailingAnnualDividendRate, open, toCurrency, averageVolume10days, expireDate, yield, algorithm, dividendRate, exDividendDate, beta, circulatingSupply, regularMarketDayLow, priceHint, currency, trailingPE, regularMarketVolume, lastMarket, maxSupply, openInterest, marketCap, volumeAllCurrencies, strikePrice, averageVolume, priceToSalesTrailing12Months, dayLow, ask, ytdReturn, askSize, volume, fiftyTwoWeekHigh, forwardPE, fromCurrency, fiveYearAvgDividendYield, fiftyTwoWeekLow, bid, dividendYield, bidSize, dayHigh, annualHoldingsTurnover, enterpriseToRevenue, beta3Year, profitMargins, enterpriseToEbitda, 52WeekChange, morningStarRiskRating, forwardEps, revenueQuarterlyGrowth, sharesOutstanding, fundInceptionDate, annualReportExpenseRatio, bookValue, sharesShort, sharesPercentSharesOut, fundFamily, lastFiscalYearEnd, heldPercentInstitutions, netIncomeToCommon, trailingEps, lastDividendValue, SandP52WeekChange, priceToBook, heldPercentInsiders, shortRatio, sharesShortPreviousMonthDate, floatShares, enterpriseValue, threeYearAverageReturn, lastSplitFactor, legalType, lastDividendDate, morningStarOverallRating, earningsQuarterlyGrowth, pegRatio, lastCapGain, shortPercentOfFloat, sharesShortPriorMonth, impliedSharesOutstanding, fiveYearAverageReturn, and regularMarketPrice. -* -v : Amount to allocate to portfolio. -* --pie : Display a pie chart for weights +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights - -### 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 -Returns an equally weighted portfolio where the weights are 1/number of stocks. -```` -equal_weight [-v --value VALUE] [--pie] -```` -* -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 [-v --value VALUE] [--pie] -```` -* -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 [-v --value VALUE] [--pie] -```` -* -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 +## Mean Variance Optimization These approaches are based off of the efficient frontier approach, which is meant to solve the following optimization problem. - +Minimize: -With constraints: +Subject to: , and - +* 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 specific, please submit a feature request, or if you can write it, feel free to add a PR! +All of these commands use [PyPortFolioOpt](#https://pyportfolioopt.readthedocs.io/en/latest/index.html) package. -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. +### maxsharpe -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. - -### max_sharpe -The sharpe ratio is defined as - -(Mean Returns - Risk Free Rate)/(Standard Deviation of Returns) - -The usage is: ```` -max_sharpe [-p PERIOD] [-v --value VALUE] [--pie] +usage: max_sharpe [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-v VALUE] [--pie] [-r RISK_FREE_RATE] +```` + +Maximise the Sharpe Ratio. The result is also referred to as the tangency portfolio, as it is the portfolio for which the capital market line is tangent to the efficient frontier. This is a convex optimization problem after making a certain variable substitution. See Cornuejols and Tutuncu (2006) for more. The sharpe ratio is defined as (Mean Returns - Risk Free Rate)/(Standard Deviation of Returns). + +* -p : Amount of time to retrieve data from yfinance. Default: 3mo. +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights + + +### minvol + +```` +usage: minvol [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-v 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] [-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] [-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. 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. +* -p : Amount of time to retrieve data from yfinance. Default: 3mo. +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights -### eff_ret -This portfolio minimizes the risk at a given return level -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. 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 +### maxquadutil + +```` +usage: maxquadutil [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-v VALUE] [--pie] [-r RISK_AVERSION] [-n] +```` + +Maximises the quadratic utility, given some risk aversion. + +* -p : Amount of time to retrieve data from yfinance. Default: 3mo. +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights +* -r : Risk aversion parameter +* -n : Whether the portfolio should be market neutral (weights sum to zero). Defaults: False. + + +### effret + +```` +usage: effret [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-v VALUE] [--pie] [-t TARGET_RETURN] [-n] +```` + +Calculate the 'Markowitz portfolio', minimising volatility for a given target return. + +* -p : Amount of time to retrieve data from yfinance. Default: 3mo. +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights +* -t : The desired return of the resulting portfolio +* -n : Whether the portfolio should be market neutral (weights sum to zero). Defaults: False. + + +### effrisk + +```` +usage: effrisk [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-v VALUE] [--pie] [-t TARGET_VOLATILITY] [-n] +```` + +Maximise return for a target risk. The resulting portfolio will have a volatility less than the target (but not guaranteed to be equal). + +* -p : Amount of time to retrieve data from yfinance. Default: 3mo. +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights +* -t : The desired maximum volatility of the resultingportfolio +* -n : Whether the portfolio should be market neutral (weights sum to zero). Defaults: False. + + +### ef + +```` +usage: ef [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-n N_PORT] +```` + 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. + +* -n : Number of portfolios to simulate. Default 300. +* -p : 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. -##Sample Usage + + +## 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: ```` From 1878d997869a2209587386707ceb40472d4042c2 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 02:24:53 +0100 Subject: [PATCH 045/112] Add nice plots and stuff --- .../portfolio_optimization/README.md | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md index 2825bf9311b..d3591364c21 100644 --- a/gamestonk_terminal/portfolio_optimization/README.md +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -60,6 +60,8 @@ Returns an equally weighted portfolio * -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. * --pie : Display a pie chart for weights +![equal](https://user-images.githubusercontent.com/25267873/114958292-9771e080-9e5a-11eb-9de6-1c2dfbe1e27b.png) + ### property @@ -73,6 +75,8 @@ Returns a portfolio that is weighted based on a selected property info * -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. * --pie : Display a pie chart for weights +![marketCap](https://user-images.githubusercontent.com/25267873/114958294-980a7700-9e5a-11eb-9db4-d06e6a1e8981.png) + ## Mean Variance Optimization @@ -90,6 +94,7 @@ 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 these commands use [PyPortFolioOpt](#https://pyportfolioopt.readthedocs.io/en/latest/index.html) package. + ### maxsharpe ```` @@ -102,6 +107,8 @@ Maximise the Sharpe Ratio. The result is also referred to as the tangency portfo * -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. * --pie : Display a pie chart for weights +maxsharpe + ### minvol @@ -115,6 +122,9 @@ This portfolio minimizes the total volatility, which also means it has the small * -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. * --pie : Display a pie chart for weights +![min_vol](https://user-images.githubusercontent.com/25267873/114958290-96d94a00-9e5a-11eb-9b7c-deba8a8e7249.png) +min_vol2 + ### maxquadutil @@ -130,6 +140,8 @@ Maximises the quadratic utility, given some risk aversion. * -r : Risk aversion parameter * -n : Whether the portfolio should be market neutral (weights sum to zero). Defaults: False. +maxquadutil + ### effret @@ -145,6 +157,8 @@ Calculate the 'Markowitz portfolio', minimising volatility for a given target re * -t : The desired return of the resulting portfolio * -n : Whether the portfolio should be market neutral (weights sum to zero). Defaults: False. +effret + ### effrisk @@ -160,6 +174,9 @@ Maximise return for a target risk. The resulting portfolio will have a volatilit * -t : The desired maximum volatility of the resultingportfolio * -n : Whether the portfolio should be market neutral (weights sum to zero). Defaults: False. +![effrisk](https://user-images.githubusercontent.com/25267873/114958284-95a81d00-9e5a-11eb-8626-988505db8890.png) +effrisk2 + ### ef @@ -172,31 +189,4 @@ This function plots random portfolios based on their risk and returns and shows * -n : Number of portfolios to simulate. Default 300. * -p : 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. - - -## 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`. 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. -```` -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 omitted from the chart. +![ef](https://user-images.githubusercontent.com/25267873/114958281-93de5980-9e5a-11eb-967b-d37ddd4f3e73.png) From 64bdc1da0cd1180784b5062a069b2a6c98ac9ca8 Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 02:26:33 +0100 Subject: [PATCH 046/112] more fixes here and there --- .../portfolio_optimization/optimizer_view.py | 45 +++++++++++++------ .../portfolio_optimization/po_controller.py | 9 ++-- .../technical_analysis/README.md | 2 +- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py index 92ee5417fb9..af081ca4708 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -10,7 +10,11 @@ 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.helper_funcs import ( + parse_known_args_and_warn, + plot_autoscale, + check_non_negative, +) from gamestonk_terminal.portfolio_optimization.optimizer_helper import ( process_stocks, prepare_efficient_frontier, @@ -130,7 +134,7 @@ def property_weighting(stocks: List[str], other_args: List[str]): parser.add_argument( "-p", "--property", - required=True, + required=True if "-h" not in other_args else False, type=check_valid_property_type, dest="property", help="""Property info to weigh. Use one of: @@ -267,7 +271,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): other_args.insert(0, "-r") parser.add_argument( "-r", - "--risk_free_rate", + "--risk-free-rate", type=float, dest="risk_free_rate", default=0.02, @@ -281,7 +285,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): other_args.insert(0, "-t") parser.add_argument( "-t", - "--target_volatility", + "--target-volatility", type=float, dest="target_volatility", default=0.1, @@ -289,7 +293,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): ) parser.add_argument( "-n", - "--market_neutral", + "--market-neutral", action="store_true", default=False, dest="market_neutral", @@ -303,7 +307,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): other_args.insert(0, "-t") parser.add_argument( "-t", - "--target_return", + "--target-return", type=float, dest="target_return", default=0.1, @@ -311,7 +315,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): ) parser.add_argument( "-n", - "--market_neutral", + "--market-neutral", action="store_true", default=False, dest="market_neutral", @@ -325,7 +329,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): other_args.insert(0, "-r") parser.add_argument( "-r", - "--risk_aversion", + "--risk-aversion", type=float, dest="risk_aversion", default=1, @@ -333,7 +337,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): ) parser.add_argument( "-n", - "--market_neutral", + "--market-neutral", action="store_true", default=False, dest="market_neutral", @@ -357,7 +361,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): if port_type == "max_sharpe": ef_opt = dict(ef.max_sharpe(ns_parser.risk_free_rate)) - s_title = f"{sp} Weights that maximize Sharpe ration with risk free level of {ns_parser.risk_free_rate}" + s_title = f"{sp} Weights that maximize Sharpe ratio with risk free level of {ns_parser.risk_free_rate}" elif port_type == "min_volatility": ef_opt = dict(ef.min_volatility()) @@ -407,7 +411,12 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): def show_ef(stocks: List[str], other_args: List[str]): - parser = argparse.ArgumentParser(add_help=False, prog="ef") + parser = argparse.ArgumentParser( + add_help=False, + prog="ef", + description="""This function plots random portfolios based + on their risk and returns and shows the efficient frontier.""", + ) parser.add_argument( "-p", @@ -418,15 +427,23 @@ def show_ef(stocks: List[str], other_args: List[str]): choices=period_choices, ) parser.add_argument( - "-n", default=300, dest="n_port", help="number of portfolios to simulate" + "-n", + "--number-portfolios", + default=300, + type=check_non_negative, + dest="n_port", + help="number of portfolios to simulate", ) try: + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-n") ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return if len(stocks) < 2: - print("Please have at least 2 loaded tickers to calculate weights.") + print("Please have at least 2 loaded tickers to calculate weights.\n") return stock_prices = process_stocks(stocks, ns_parser.period) @@ -449,7 +466,7 @@ def show_ef(stocks: List[str], other_args: List[str]): 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.set_title(f"Efficient Frontier simulating {ns_parser.n_port} portfolios") ax.legend() plt.tight_layout() plt.grid(b=True, which="major", color="#666666", linestyle="-") diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index 24a93bcdd5e..a7ef204f128 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -61,6 +61,7 @@ class PortfolioOptimization: " property weight according to selected info property (e.g. marketCap)" ) print("") + print("Mean Variance Optimization:") print( " maxsharpe optimizes for maximal Sharpe ratio (a.k.a the tangency portfolio)" ) @@ -151,11 +152,11 @@ class PortfolioOptimization: @staticmethod def add_stocks(self, other_args: List[str]): - """ Add ticker to current list for optimization""" + """ Add ticker or Select tickes for portfolio to be optimized """ parser = argparse.ArgumentParser( add_help=False, - prog="add", - description="""Add tickers for optimizing.""", + prog="add/select", + description="""Add/Select tickers for portfolio to be optimized.""", ) parser.add_argument( "-t", @@ -163,7 +164,7 @@ class PortfolioOptimization: dest="add_tickers", type=lambda s: [str(item).upper() for item in s.split(",")], default=[], - help="add tickers to optimzation.", + help="tickers to be used in the portfolio to optimize.", ) try: if other_args: diff --git a/gamestonk_terminal/technical_analysis/README.md b/gamestonk_terminal/technical_analysis/README.md index c35ebb6bdaa..0e5c511d850 100644 --- a/gamestonk_terminal/technical_analysis/README.md +++ b/gamestonk_terminal/technical_analysis/README.md @@ -43,7 +43,7 @@ This menu aims to perform a technical analysis on a pre-loaded ticker chart, and * [obv](#obv) - on balance volume -**S/O to https://github.com/twopirllc/pandas-ta** _Owing to this library, it is fairly easy to add other technical indicators. So, let me know if there's any that you would like. Personally I use mostly these ones, hence why I didn't add more._ +**S/O to https://github.com/twopirllc/pandas-ta** _Owing to this library, it is fairly easy to add other technical indicators. So, let us know if there's any other that you would like._ ## view ``` From 31959452c7a43d681d9af4264e73d48510c8d08a Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 02:29:29 +0100 Subject: [PATCH 047/112] Update README.md --- gamestonk_terminal/comparison_analysis/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gamestonk_terminal/comparison_analysis/README.md b/gamestonk_terminal/comparison_analysis/README.md index f00b902183d..c60dfd305d0 100644 --- a/gamestonk_terminal/comparison_analysis/README.md +++ b/gamestonk_terminal/comparison_analysis/README.md @@ -32,6 +32,8 @@ This menu aims to compare a pre-loaded stock with similar companies, and the usa * brief performance (e.g. Perf Week, Perf YTD, Volatility M) [Finviz] * [technical](#technical) * brief technical (e.g. Beta, SMA50, 52W Low, RSI, Change) [Finviz] +* [> po](portfolio_optimization/README.md) + * portfolio optimization for selected tickers ## get From 7b94769b793b486f0b81b3eaaa21587467ec5c09 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 02:30:15 +0100 Subject: [PATCH 048/112] Update README.md --- gamestonk_terminal/screener/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gamestonk_terminal/screener/README.md b/gamestonk_terminal/screener/README.md index 6271a7684db..3135fb3f981 100644 --- a/gamestonk_terminal/screener/README.md +++ b/gamestonk_terminal/screener/README.md @@ -23,8 +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 +* [> po](portfolio_optimization/README.md) + * portfolio optimization for last screened tickers ## view @@ -190,4 +190,4 @@ usage: po ticker1 ticker2 ... ```` example: po aapl msft tsla gme -```` \ No newline at end of file +```` From 5d9ee5c8974fe4a0f10d3114a8cdab9e636c7c71 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 02:42:43 +0100 Subject: [PATCH 049/112] Update ROADMAP.md --- ROADMAP.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index f5c466f3a9a..073316e0213 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -14,6 +14,7 @@ * [Exploratory Data Analysis](#Exploratory-Data-Analysis) * [Residual Analysis](#Residual-Analysis) * [Portfolio Analysis](#Portfolio-Analysis) +* [Portfolio Optimization](#Portfolio-Optimization) * [FRED](#FRED) * [Options](#Options) * [Credit Analysis](#Credit-Analysis) @@ -110,6 +111,7 @@ * [x] Add multi-ticker sentiment comparison (@didier) - [PR #250](https://github.com/DidierRLopes/GamestonkTerminal/pull/250) * [x] Get similar companies from Finviz based on Industry and Sector (and possibly country) from Finviz (@didier) - [PR #323](https://github.com/DidierRLopes/GamestonkTerminal/pull/323) * [x] Output brief screen (overview, performance, financial, valuation) across similar companies (@didier) - [PR #323](https://github.com/DidierRLopes/GamestonkTerminal/pull/323) +* [x] Add command to take tickers to portfolio optimization (@jmaslek) - [PR #329](https://github.com/DidierRLopes/GamestonkTerminal/pull/329) **NEXT** @@ -142,13 +144,17 @@ * [ ] Add Brokers (td, webull, etc) (@jmaslek) * [ ] Refactoring * [ ] Summaries / tear sheets (@jmaslek) + ___ -##Portfolio Optimization + +## Portfolio Optimization * [x] Basic Optimization through PyPortFolioOpt(@jmaslek) - [PR #329](https://github.com/DidierRLopes/GamestonkTerminal/pull/329) +* [x] Add command to maximise the quadratic utility(@didier) - [PR #349](https://github.com/DidierRLopes/GamestonkTerminal/pull/349) **NEXT** -* [ ] Decide what features should be present * [ ] Allow for more custom optimization constrains + + --- ## FRED @@ -189,6 +195,7 @@ ___ * [x] Add README with explanation of how presets are stored and can be added by experienced users. (@didier) - [PR #314](https://github.com/DidierRLopes/GamestonkTerminal/pull/314) * [x] Add screener signals (e.g. top gainers, new highs, most volatile, oversold, major news, ...) from Finviz (@didier) - [PR #314](https://github.com/DidierRLopes/GamestonkTerminal/pull/314) * [x] Plot screener historical using Yahoo Finance data (@didier) - [PR #319](https://github.com/DidierRLopes/GamestonkTerminal/pull/319) +* [x] Add command to take tickers to portfolio optimization (@didier) - [PR #349](https://github.com/DidierRLopes/GamestonkTerminal/pull/349) **NEXT** From 886e1830c0e4a163677cc4df34521f46d90f30a7 Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 02:58:12 +0100 Subject: [PATCH 050/112] im a picky guy, sorry --- gamestonk_terminal/comparison_analysis/ca_controller.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py index e0a339202c2..499b6c9cd39 100644 --- a/gamestonk_terminal/comparison_analysis/ca_controller.py +++ b/gamestonk_terminal/comparison_analysis/ca_controller.py @@ -84,8 +84,6 @@ class ComparisonAnalysisController: if self.similar: print(f"[{self.user}] Similar Companies: {', '.join(self.similar)}") - # else: - # print(f"No similar companies [{self.user}]") print("\nComparison Analysis Mode:") print(" help show this comparison analysis menu again") @@ -110,8 +108,9 @@ class ComparisonAnalysisController: print(" performance brief performance comparison") print(" technical brief technical comparison") print("") - print(" > po portfolio optimization for selected tickers") - print("") + if self.similar: + print(" > po portfolio optimization for selected tickers") + print("") return @staticmethod From 1ceb93861051a42cc4038963ed234716d05e0b56 Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Fri, 16 Apr 2021 10:02:31 +0200 Subject: [PATCH 051/112] change handling and listing of the latest news from SeekingAlpha --- .../discovery/disc_controller.py | 10 ++--- .../discovery/seeking_alpha_model.py | 18 ++++----- .../discovery/seeking_alpha_view.py | 37 ++++++++++++------- 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index fa0f893232f..9feec08025a 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -46,7 +46,7 @@ class DiscoveryController: "valuation", "performance", "spectrum", - "market_news", + "latest", "news_article" ] @@ -89,7 +89,7 @@ class DiscoveryController: print(" valuation valuation of sectors, industry, country [Finviz]") print(" performance performance of sectors, industry, country [Finviz]") print(" spectrum spectrum of sectors, industry, country [Finviz]") - print(" market_news latest news [SeekingAlpha]") + print(" latest latest news [SeekingAlpha]") print(" news_article news article [SeekingAlpha]") print("") @@ -191,9 +191,9 @@ class DiscoveryController: other_args, "spectrum" ) - def call_market_news(self, other_args: List[str]): - """Process market_news command""" - seeking_alpha_view.articles_list_view(other_args) + def call_latest(self, other_args: List[str]): + """Process latest command""" + seeking_alpha_view.latest_news_view(other_args) def call_news_article(self, other_args: List[str]): """Process news_article command""" diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index 06ec76e8b15..aa74045c041 100755 --- a/gamestonk_terminal/discovery/seeking_alpha_model.py +++ b/gamestonk_terminal/discovery/seeking_alpha_model.py @@ -89,13 +89,13 @@ def get_articles_html(url_articles: str) -> str: return articles_html -def get_article_list(pages: int) -> list: - """Returns a list of recent articles +def get_article_list(num: int) -> list: + """Returns a list of latest articles Parameters ---------- pages : int - Number of pages + Number of articles Returns ------- @@ -104,8 +104,9 @@ def get_article_list(pages: int) -> list: """ articles = list() - url_articles = "https://seekingalpha.com/market-news" - for idx in range(0, pages): + page = 1 + url_articles = f"https://seekingalpha.com/market-news/{page}" + while len(articles) < num: text_soup_articles = BeautifulSoup( get_articles_html(url_articles), "lxml", @@ -121,14 +122,13 @@ def get_article_list(pages: int) -> list: article_id = article_url.split('/')[2].split('-')[0] articles.append({ 'title': item.text, - 'date': item_row['data-last-date'], + 'publishedAt': item_row['data-last-date'], 'url': "https://seekingalpha.com"+article_url, 'id': article_id }) - url_articles = ( - f"https://seekingalpha.com/market-news/{idx+1}" - ) + page += 1 + url_articles = f"https://seekingalpha.com/market-news/{page}" return articles diff --git a/gamestonk_terminal/discovery/seeking_alpha_view.py b/gamestonk_terminal/discovery/seeking_alpha_view.py index f54714b11cc..eff8c341112 100644 --- a/gamestonk_terminal/discovery/seeking_alpha_view.py +++ b/gamestonk_terminal/discovery/seeking_alpha_view.py @@ -67,38 +67,49 @@ def earnings_release_dates_view(other_args: List[str]): print("") -def articles_list_view(other_args: List[str]): - """Prints a news article list +def latest_news_view(other_args: List[str]): + """Prints the latest news article list Parameters ---------- other_args : List[str] - argparse other args - ["-p", "20", "-n", "5"] + argparse other args - ["-n", "5"] """ parser = argparse.ArgumentParser( add_help=False, - prog="up_articles", - description="""Latest articles. [Source: Seeking Alpha]""", + prog="latest", + description="""Latest news articles. [Source: Seeking Alpha]""", ) parser.add_argument( - "-p", - "--pages", + "-n", + "--num", action="store", - dest="n_pages", + dest="n_num", type=check_positive, - default=1, - help="Number of pages from Seeking Alpha website.", + default=10, + help="number of latest articles being printed.", ) ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return - articles = seeking_alpha_model.get_article_list(ns_parser.n_pages) - for article in articles: - print(article['id'], '-', article['title'], '(', article['url'], ')') + articles = seeking_alpha_model.get_article_list(ns_parser.n_num) + for idx, article in enumerate(articles): + print( + article["publishedAt"].replace("T", " ").replace("Z", ""), + "-", + article['id'], + "-", + article["title"], + ) + print(article["url"]) + print("") + + if idx >= ns_parser.n_num - 1: + break def news_article_view(other_args: List[str]): From f2bc9c1cbe689faf42b7c62be691e8a78bda1194 Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Fri, 16 Apr 2021 10:44:48 +0200 Subject: [PATCH 052/112] add trending news list, from Seeking Alpha --- .../discovery/disc_controller.py | 6 +++ .../discovery/seeking_alpha_model.py | 37 +++++++++++++++ .../discovery/seeking_alpha_view.py | 45 +++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index 9feec08025a..12bc5149ad3 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -47,6 +47,7 @@ class DiscoveryController: "performance", "spectrum", "latest", + "trending", "news_article" ] @@ -90,6 +91,7 @@ class DiscoveryController: print(" performance performance of sectors, industry, country [Finviz]") print(" spectrum spectrum of sectors, industry, country [Finviz]") print(" latest latest news [SeekingAlpha]") + print(" trending trending news [SeekingAlpha]") print(" news_article news article [SeekingAlpha]") print("") @@ -195,6 +197,10 @@ class DiscoveryController: """Process latest command""" seeking_alpha_view.latest_news_view(other_args) + def call_trending(self, other_args: List[str]): + """Process trending command""" + seeking_alpha_view.trending_news_view(other_args) + def call_news_article(self, other_args: List[str]): """Process news_article command""" seeking_alpha_view.news_article_view(other_args) diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index aa74045c041..684e009390a 100755 --- a/gamestonk_terminal/discovery/seeking_alpha_model.py +++ b/gamestonk_terminal/discovery/seeking_alpha_model.py @@ -133,6 +133,43 @@ def get_article_list(num: int) -> list: return articles +def get_trending_list(num: int) -> list: + """Returns a list of trending articles + + Parameters + ---------- + pages : int + Number of articles + + Returns + ------- + list + Trending articles list + """ + + articles = list() + url_articles = "https://seekingalpha.com/news/trending_news" + response = requests.get(url_articles, headers={"User-Agent": get_user_agent()}) + + # Check that the API response was successful + if response.status_code != 200: + print("Invalid response\n") + else: + for item in response.json(): + article_url = item['uri'] + if not article_url.startswith('/news/'): + continue + article_id = article_url.split('/')[2].split('-')[0] + articles.append({ + 'title': item['title'], + 'publishedAt': item['publish_on'], + 'url': "https://seekingalpha.com"+article_url, + 'id': article_id + }) + + return articles + + def get_article_data(article_id: int) -> dict: """Returns an article diff --git a/gamestonk_terminal/discovery/seeking_alpha_view.py b/gamestonk_terminal/discovery/seeking_alpha_view.py index eff8c341112..0d3764c1a34 100644 --- a/gamestonk_terminal/discovery/seeking_alpha_view.py +++ b/gamestonk_terminal/discovery/seeking_alpha_view.py @@ -112,6 +112,51 @@ def latest_news_view(other_args: List[str]): break +def trending_news_view(other_args: List[str]): + """Prints the trending news article list + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-n", "5"] + """ + + parser = argparse.ArgumentParser( + add_help=False, + prog="trending", + description="""Trending news articles. [Source: Seeking Alpha]""", + ) + + parser.add_argument( + "-n", + "--num", + action="store", + dest="n_num", + type=check_positive, + default=10, + help="number of trending articles being printed.", + ) + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + articles = seeking_alpha_model.get_trending_list(ns_parser.n_num) + for idx, article in enumerate(articles): + print( + article["publishedAt"].replace("T", " ").replace("Z", ""), + "-", + article['id'], + "-", + article["title"], + ) + print(article["url"]) + print("") + + if idx >= ns_parser.n_num - 1: + break + + def news_article_view(other_args: List[str]): """Prints a news article From 7d3e9de61f4a4cffe2fc4ac52e6263b8398aa8f3 Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Fri, 16 Apr 2021 11:00:50 +0200 Subject: [PATCH 053/112] small fixes to the article print output --- gamestonk_terminal/discovery/seeking_alpha_model.py | 3 ++- gamestonk_terminal/discovery/seeking_alpha_view.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index 684e009390a..5db84d9bb11 100755 --- a/gamestonk_terminal/discovery/seeking_alpha_model.py +++ b/gamestonk_terminal/discovery/seeking_alpha_model.py @@ -192,7 +192,8 @@ def get_article_data(article_id: int) -> dict: article = { 'title': jdata['data']['attributes']['title'], - 'date': jdata['data']['attributes']['lastModified'], + 'publishedAt': jdata['data']['attributes']['lastModified'], + 'url': "https://seekingalpha.com"+jdata['data']['links']['self'], 'content': content } diff --git a/gamestonk_terminal/discovery/seeking_alpha_view.py b/gamestonk_terminal/discovery/seeking_alpha_view.py index 0d3764c1a34..f717426f66c 100644 --- a/gamestonk_terminal/discovery/seeking_alpha_view.py +++ b/gamestonk_terminal/discovery/seeking_alpha_view.py @@ -189,5 +189,7 @@ def news_article_view(other_args: List[str]): return article = seeking_alpha_model.get_article_data(ns_parser.n_id) - print(article['date'], article['title']) + print(article['publishedAt'], " ", article['title']) + print(article["url"]) + print("") print(article['content']) From ac08309abcc617bfb368adeaa1128f6280a996fd Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Fri, 16 Apr 2021 11:34:12 +0200 Subject: [PATCH 054/112] comply with cosmetics requirements --- .../discovery/disc_controller.py | 2 +- .../discovery/seeking_alpha_model.py | 55 ++++++++++--------- .../discovery/seeking_alpha_view.py | 14 ++--- 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index 12bc5149ad3..a9a66116107 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -48,7 +48,7 @@ class DiscoveryController: "spectrum", "latest", "trending", - "news_article" + "news_article", ] def __init__(self): diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index 5db84d9bb11..7e85ca8ea70 100755 --- a/gamestonk_terminal/discovery/seeking_alpha_model.py +++ b/gamestonk_terminal/discovery/seeking_alpha_model.py @@ -1,6 +1,7 @@ """ Seeking Alpha Model """ __docformat__ = "numpy" +from typing import List import requests from bs4 import BeautifulSoup import pandas as pd @@ -89,7 +90,7 @@ def get_articles_html(url_articles: str) -> str: return articles_html -def get_article_list(num: int) -> list: +def get_article_list(num: int) -> List[dict]: """Returns a list of latest articles Parameters @@ -103,7 +104,7 @@ def get_article_list(num: int) -> list: Latest articles list """ - articles = list() + articles: List[dict] = [] page = 1 url_articles = f"https://seekingalpha.com/market-news/{page}" while len(articles) < num: @@ -116,16 +117,18 @@ def get_article_list(num: int) -> list: item = item_row.find("a", {"class": "add-source-assigned"}) if item is None: continue - article_url = item['href'] - if not article_url.startswith('/news/'): + article_url = item["href"] + if not article_url.startswith("/news/"): continue - article_id = article_url.split('/')[2].split('-')[0] - articles.append({ - 'title': item.text, - 'publishedAt': item_row['data-last-date'], - 'url': "https://seekingalpha.com"+article_url, - 'id': article_id - }) + article_id = article_url.split("/")[2].split("-")[0] + articles.append( + { + "title": item.text, + "publishedAt": item_row["data-last-date"], + "url": "https://seekingalpha.com" + article_url, + "id": article_id, + } + ) page += 1 url_articles = f"https://seekingalpha.com/market-news/{page}" @@ -156,16 +159,18 @@ def get_trending_list(num: int) -> list: print("Invalid response\n") else: for item in response.json(): - article_url = item['uri'] - if not article_url.startswith('/news/'): + article_url = item["uri"] + if not article_url.startswith("/news/"): continue - article_id = article_url.split('/')[2].split('-')[0] - articles.append({ - 'title': item['title'], - 'publishedAt': item['publish_on'], - 'url': "https://seekingalpha.com"+article_url, - 'id': article_id - }) + article_id = article_url.split("/")[2].split("-")[0] + articles.append( + { + "title": item["title"], + "publishedAt": item["publish_on"], + "url": "https://seekingalpha.com" + article_url, + "id": article_id, + } + ) return articles @@ -187,14 +192,14 @@ def get_article_data(article_id: int) -> dict: article_url = f"https://seekingalpha.com/api/v3/news/{article_id}" response = requests.get(article_url, headers={"User-Agent": get_user_agent()}) jdata = response.json() - content = jdata['data']['attributes']['content'].replace('', '\n') + content = jdata["data"]["attributes"]["content"].replace("", "\n") content = BeautifulSoup(content, features="html.parser").get_text() article = { - 'title': jdata['data']['attributes']['title'], - 'publishedAt': jdata['data']['attributes']['lastModified'], - 'url': "https://seekingalpha.com"+jdata['data']['links']['self'], - 'content': content + "title": jdata["data"]["attributes"]["title"], + "publishedAt": jdata["data"]["attributes"]["lastModified"], + "url": "https://seekingalpha.com" + jdata["data"]["links"]["self"], + "content": content, } return article diff --git a/gamestonk_terminal/discovery/seeking_alpha_view.py b/gamestonk_terminal/discovery/seeking_alpha_view.py index f717426f66c..639e145f0d0 100644 --- a/gamestonk_terminal/discovery/seeking_alpha_view.py +++ b/gamestonk_terminal/discovery/seeking_alpha_view.py @@ -89,7 +89,7 @@ def latest_news_view(other_args: List[str]): dest="n_num", type=check_positive, default=10, - help="number of latest articles being printed.", + help="number of latest articles being printed", ) ns_parser = parse_known_args_and_warn(parser, other_args) @@ -101,7 +101,7 @@ def latest_news_view(other_args: List[str]): print( article["publishedAt"].replace("T", " ").replace("Z", ""), "-", - article['id'], + article["id"], "-", article["title"], ) @@ -134,7 +134,7 @@ def trending_news_view(other_args: List[str]): dest="n_num", type=check_positive, default=10, - help="number of trending articles being printed.", + help="number of trending articles being printed", ) ns_parser = parse_known_args_and_warn(parser, other_args) @@ -146,7 +146,7 @@ def trending_news_view(other_args: List[str]): print( article["publishedAt"].replace("T", " ").replace("Z", ""), "-", - article['id'], + article["id"], "-", article["title"], ) @@ -179,7 +179,7 @@ def news_article_view(other_args: List[str]): dest="n_id", type=check_positive, default=-1, - help="Article number found on Seeking Alpha website.", + help="article number found on Seeking Alpha website", ) ns_parser = parse_known_args_and_warn(parser, other_args) @@ -189,7 +189,7 @@ def news_article_view(other_args: List[str]): return article = seeking_alpha_model.get_article_data(ns_parser.n_id) - print(article['publishedAt'], " ", article['title']) + print(article["publishedAt"], " ", article["title"]) print(article["url"]) print("") - print(article['content']) + print(article["content"]) From c3a598dddeb3aa831426681ed63603b04af23d5c Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Fri, 16 Apr 2021 14:50:40 +0200 Subject: [PATCH 055/112] introduce latest ratings changes from MarketBeat --- .../discovery/disc_controller.py | 7 ++ .../discovery/marketbeat_model.py | 93 +++++++++++++++++++ .../discovery/marketbeat_view.py | 54 +++++++++++ 3 files changed, 154 insertions(+) create mode 100755 gamestonk_terminal/discovery/marketbeat_model.py create mode 100644 gamestonk_terminal/discovery/marketbeat_view.py diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index 07bb33a3302..de17f94334a 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -20,6 +20,7 @@ from gamestonk_terminal.discovery import ( spachero_view, unusual_whales_view, yahoo_finance_view, + marketbeat_view, ) @@ -46,6 +47,7 @@ class DiscoveryController: "valuation", "performance", "spectrum", + "ratings", ] def __init__(self): @@ -87,6 +89,7 @@ class DiscoveryController: print(" valuation valuation of sectors, industry, country [Finviz]") print(" performance performance of sectors, industry, country [Finviz]") print(" spectrum spectrum of sectors, industry, country [Finviz]") + print(" ratings latest ratings [MarketBeat]") print("") def switch(self, an_input: str): @@ -187,6 +190,10 @@ class DiscoveryController: other_args, "spectrum" ) + def call_ratings(self, other_args: List[str]): + """Process ratings command""" + marketbeat_view.ratings_view(other_args) + def menu(): """Discovery Menu""" diff --git a/gamestonk_terminal/discovery/marketbeat_model.py b/gamestonk_terminal/discovery/marketbeat_model.py new file mode 100755 index 00000000000..b8bd10241e4 --- /dev/null +++ b/gamestonk_terminal/discovery/marketbeat_model.py @@ -0,0 +1,93 @@ +""" MarketBeat Model """ +__docformat__ = "numpy" + +from typing import List +import requests +from bs4 import BeautifulSoup + +from gamestonk_terminal.helper_funcs import get_user_agent + + +def get_ratings_html(url_ratings: str) -> str: + """Wraps HTTP requests.get for testibility + + Parameters + ---------- + url_ratings : str + Ratings URL + + Returns + ------- + str + HTML page of ratings + """ + ratings_html = requests.get( + url_ratings, headers={"User-Agent": get_user_agent()} + ).text + + return ratings_html + + +def get_ratings() -> List[dict]: + """Returns a list of ratings + + Parameters + ---------- + None + + Returns + ------- + list + Ratings list + """ + + ratings = list() + url_ratings = "https://www.marketbeat.com/ratings/" + + text_soup_ratings = BeautifulSoup( + get_ratings_html(url_ratings), + "lxml", + ) + + for stock_rows in text_soup_ratings.findAll("tr"): + tds = stock_rows.findAll("td") + if len(tds) == 8: + rating = {} + # company + td = tds[0] + company = td.find("div", {"class": "title-area"}).text + ticker = td.find("div", {"class": "ticker-area"}).text + rating["company"] = company + rating["ticker"] = ticker + # action + td = tds[1] + action = td.text + rating["action"] = action + # brokerage + td = tds[2] + brokerage = td.find("a").text + rating["brokerage"] = brokerage + # current price + td = tds[3] + if td.find("span"): + td.span.extract() + current_price = td.text + rating["current_price"] = current_price + # target price + td = tds[4] + target_price = td.text + rating["target_price"] = target_price + # new rating + td = tds[5] + new_rating = td.text + rating["rating"] = new_rating + # impact on price + td = tds[6] + if td.find("a"): + impact = td.find("a").text + else: + impact = td.text + rating["impact"] = impact + ratings.append(rating) + + return ratings diff --git a/gamestonk_terminal/discovery/marketbeat_view.py b/gamestonk_terminal/discovery/marketbeat_view.py new file mode 100644 index 00000000000..aad9b03ec5c --- /dev/null +++ b/gamestonk_terminal/discovery/marketbeat_view.py @@ -0,0 +1,54 @@ +""" MarketBeat View """ +__docformat__ = "numpy" + +import argparse +from typing import List +from gamestonk_terminal.helper_funcs import ( + check_positive, + parse_known_args_and_warn, +) + +from gamestonk_terminal.discovery import marketbeat_model + + +def ratings_view(other_args: List[str]): + """Prints a list with ratings + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-n", "5"] + """ + + parser = argparse.ArgumentParser( + add_help=False, + prog="ratings", + description="""Latest ratings. [Source: MarketBeat]""", + ) + + parser.add_argument( + "-n", + "--num", + action="store", + dest="n_num", + type=check_positive, + default=10, + help="number of ratings to print", + ) + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + ratings = marketbeat_model.get_ratings() + for idx, rating in enumerate(ratings): + print( + rating["ticker"], + rating["action"].replace(" by", ""), + rating["impact"], + rating["current_price"], + "->", + rating["target_price"], + ) + if idx >= ns_parser.n_num - 1: + break From 58dc9ee52dce121c1973892a6da11053af9f14ab Mon Sep 17 00:00:00 2001 From: Jamie Date: Fri, 16 Apr 2021 14:43:40 -0400 Subject: [PATCH 056/112] add in option chains :) --- gamestonk_terminal/config_terminal.py | 3 + gamestonk_terminal/options/README.md | 29 ++- gamestonk_terminal/options/chains_view.py | 208 ++++++++++++++++++++ gamestonk_terminal/options/op_controller.py | 9 +- gamestonk_terminal/options/volume_view.py | 2 +- 5 files changed, 246 insertions(+), 5 deletions(-) create mode 100644 gamestonk_terminal/options/chains_view.py diff --git a/gamestonk_terminal/config_terminal.py b/gamestonk_terminal/config_terminal.py index 4189a27ce4e..daf66a26be6 100644 --- a/gamestonk_terminal/config_terminal.py +++ b/gamestonk_terminal/config_terminal.py @@ -35,3 +35,6 @@ API_NEWS_TOKEN = os.getenv("GT_API_NEWS_TOKEN") or "REPLACE_ME" # Robinhood RH_USERNAME = os.getenv("GT_RH_USERNAME") or "REPLACE_ME" RH_PASSWORD = os.getenv("GT_RH_PASSWORD") or "REPLACE_ME" + +# https://tradier.com/products/market-data-api +TRADIER_TOKEN = os.getenv("GT_TRADIER_TOKEN") or "REPLACE_ME" diff --git a/gamestonk_terminal/options/README.md b/gamestonk_terminal/options/README.md index a86dceb2b2c..980be7495c4 100644 --- a/gamestonk_terminal/options/README.md +++ b/gamestonk_terminal/options/README.md @@ -1,11 +1,13 @@ # OPTIONS DATA -This menu aims to give insight on options. options can cause significant share price movement, and option pricing gives info about market sentiment for a ticker. +This menu aims to give insight on options. options can cause significant share price movement, and option pricing gives +info about market sentiment for a ticker. * [volume](#volume) * Show traded options for a given expiry [Yahoo Finance] - +* [chains](#chains) + * Display option chains for ## volume @@ -23,3 +25,26 @@ usage: volume --expiry yyyy-mm-dd Display volume graph for a date. [Source: Yahoo Finance] * -e, --expiry : expiration date. + +## chains +Display a table of options chains. + +This data requires a Tradier sandbox developer account, which can be made [here](#https://tradier.com/products/market-data-api) + +```` +usage: chains [--calls] [--puts] [-m MIN] [-M MAX] +```` +* --calls : Flag to only display call options +* --puts : Flag to display only put options +* -m : Minimum strike price to show +* -M : Maximum strike price to show + +For combines tables, call options are on the left and put options are on the right of the strike + +As an example, if I load my ticker and select an expiry, to view the options chain for strikes between $129 and $135, one would use: + +```` +chains -m 129 -M 135 +```` +Which outputs: +![im](https://user-images.githubusercontent.com/18151143/115068041-b191da00-9ebf-11eb-941e-4e255787ab94.png) diff --git a/gamestonk_terminal/options/chains_view.py b/gamestonk_terminal/options/chains_view.py new file mode 100644 index 00000000000..c05552a909e --- /dev/null +++ b/gamestonk_terminal/options/chains_view.py @@ -0,0 +1,208 @@ +"""Helper functions for the options menu""" +__docformat__ = "numpy" + +import argparse +from typing import List +import requests +import pandas as pd +import numpy as np +from tabulate import tabulate +from gamestonk_terminal.config_terminal import TRADIER_TOKEN +from gamestonk_terminal.helper_funcs import ( + check_non_negative, + parse_known_args_and_warn, +) + +option_columns = [ + "bid", + "ask", + "strike", + "bidsize", + "asksize", + "volume", + "open_interest", + "option_type", +] +greek_columns = ["delta", "gamma", "theta", "vega", "ask_iv", "bid_iv", "mid_iv"] +df_columns = option_columns + greek_columns +column_map = {"mid_iv": "iv", "open_interest": "oi", "volume": "vol"} + + +def process_chains(response: requests.models.Response) -> pd.DataFrame: + """ + Function to take in the requests.get and return a DataFrame + Parameters + ---------- + response: requests.models.Response + This is the respnose from tradier api. + + Returns + ------- + opt_chain: pd.DataFrame + Dataframe with all available options + """ + json_response = response.json() + options = json_response["options"]["option"] + + opt_chain = pd.DataFrame(columns=df_columns) + for idx, option in enumerate(options): + data = [option[col] for col in option_columns] + data += [option["greeks"][col] for col in greek_columns] + opt_chain.loc[idx, :] = data + + return opt_chain + + +def get_option_chains(symbol: str, expiry: str) -> pd.DataFrame: + """ + + Parameters + ---------- + symbol : str + Ticker to get options for + expiry : str + Expiration date in the form of "YYYY-MM-DD" + + Returns + ------- + chains: pd.DataFrame + Dataframe with options for the gievn Symbol and Expiration date + """ + params = {"symbol": symbol, "expiration": expiry, "greeks": "true"} + + headers = {"Authorization": f"Bearer {TRADIER_TOKEN}", "Accept": "application/json"} + + response = requests.get( + "https://sandbox.tradier.com/v1/markets/options/chains", + params=params, + headers=headers, + ) + if response.status_code == 401: + print("Error in request.get -- check token") + return + + chains = process_chains(response) + return chains + + +def display_chains(symbol: str, expiry: str, other_args: List[str]): + + parser = argparse.ArgumentParser( + prog="chains", add_help=False, description="Display options chains" + ) + + parser.add_argument( + "--calls", + action="store_true", + default=False, + dest="calls_only", + help="Flag to show calls only", + ) + + parser.add_argument( + "--puts", + action="store_true", + default=False, + dest="puts_only", + help="Flag to show puts only", + ) + parser.add_argument( + "-m", + "--min", + dest="min_sp", + type=check_non_negative, + default=-1, + help="minimum strike price to consider.", + ) + parser.add_argument( + "-M", + "--max", + dest="max_sp", + type=check_non_negative, + default=-1, + help="maximum strike price to consider.", + ) + + chains_df = get_option_chains(symbol, expiry) + + default_columns = [ + "mid_iv", + "delta", + "volume", + "open_interest", + "bid", + "ask", + "strike", + "option_type", + ] + chains_df = chains_df[default_columns].rename(columns=column_map) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + if ns_parser.max_sp == -1 and ns_parser.min_sp == -1: + min_strike = np.percentile(chains_df["strike"], 25) + max_strike = np.percentile(chains_df["strike"], 75) + else: + min_strike = ns_parser.min_sp + max_strike = ns_parser.max_sp + + chains_df = chains_df[chains_df["strike"] >= min_strike] + chains_df = chains_df[chains_df["strike"] <= max_strike] + calls_df = chains_df[chains_df.option_type == "call"].drop( + columns=["option_type"] + ) + puts_df = chains_df[chains_df.option_type == "put"].drop( + columns=["option_type"] + ) + + if ns_parser.calls_only: + print( + tabulate( + calls_df, + headers=calls_df.columns, + tablefmt="grid", + showindex=False, + floatfmt=".2f", + ) + ) + return + + if ns_parser.puts_only: + print( + tabulate( + puts_df, + headers=puts_df.columns, + tablefmt="grid", + showindex=False, + floatfmt=".2f", + ) + ) + return + + puts_df = puts_df[puts_df.columns[::-1]] + chain_table = calls_df.merge(puts_df, on="strike") + + headers = [ + col.strip("_x") + if col.endswith("_x") + else col.strip("_y") + if col.endswith("_y") + else col + for col in chain_table.columns + ] + print( + tabulate( + chain_table, + headers=headers, + tablefmt="fancy_grid", + showindex=False, + floatfmt=".2f", + ) + ) + + except Exception as e: + print(e) + print("") + return diff --git a/gamestonk_terminal/options/op_controller.py b/gamestonk_terminal/options/op_controller.py index e373ba1f677..4caa51dbd63 100644 --- a/gamestonk_terminal/options/op_controller.py +++ b/gamestonk_terminal/options/op_controller.py @@ -9,7 +9,7 @@ import yfinance as yf from prompt_toolkit.completion import NestedCompleter from gamestonk_terminal.helper_funcs import get_flair, parse_known_args_and_warn from gamestonk_terminal import feature_flags as gtff -from gamestonk_terminal.options import volume_view +from gamestonk_terminal.options import volume_view, chains_view from gamestonk_terminal.menu import session @@ -17,7 +17,7 @@ class OptionsController: """Options Controller class.""" # Command choices - CHOICES = ["help", "q", "quit", "exp", "voi", "vcalls", "vputs"] + CHOICES = ["help", "q", "quit", "exp", "voi", "vcalls", "vputs", "chains"] def __init__(self, ticker: str, last_adj_close_price: float): """Construct data.""" @@ -93,6 +93,8 @@ class OptionsController: print(" vcalls calls volume + open interest plot") print(" vputs puts volume + open interest plot") print("") + print(" chains display option chains") + print("") return def switch(self, an_input: str): @@ -158,6 +160,9 @@ class OptionsController: self.options.puts, ) + def call_chains(self, other_args): + chains_view.display_chains(self.ticker, self.expiry_date, other_args) + def menu(ticker: str, last_adj_close_price: float): """ Options Menu. """ diff --git a/gamestonk_terminal/options/volume_view.py b/gamestonk_terminal/options/volume_view.py index a20e4cafc54..a4f3062a960 100644 --- a/gamestonk_terminal/options/volume_view.py +++ b/gamestonk_terminal/options/volume_view.py @@ -2,12 +2,12 @@ __docformat__ = "numpy" from bisect import bisect_left -import seaborn as sns from typing import List import argparse import pandas as pd import matplotlib.pyplot as plt import numpy as np +import seaborn as sns from gamestonk_terminal.helper_funcs import ( plot_autoscale, From 245f3ddc6df7f121e0ec48b9a4423dfd0baec63b Mon Sep 17 00:00:00 2001 From: Jamie Date: Fri, 16 Apr 2021 14:47:31 -0400 Subject: [PATCH 057/112] update main readme with API key --- README.md | 6 +++++- gamestonk_terminal/feature_flags.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 54c5089fe7f..84a6007fdd1 100644 --- a/README.md +++ b/README.md @@ -257,10 +257,14 @@ These are the ones where a key is necessary: * Financial Modeling Prep: https://financialmodelingprep.com/developer * FRED: https://fred.stlouisfed.org/docs/api/api_key.html * News API: https://newsapi.org + * Tradier: https://developer.tradier.com/getting_started When these are obtained, don't forget to update [config_terminal.py](/gamestonk_terminal/config_terminal.py). -Alternatively, you can also set them to the following environment variables: GT_API_KEY_ALPHAVANTAGE, GT_API_KEY_FINANCIALMODELINGPREP, GT_API_KEY_QUANDL, GT_API_REDDIT_CLIENT_ID, GT_API_REDDIT_CLIENT_SECRET, GT_API_REDDIT_USERNAME, GT_API_REDDIT_USER_AGENT, GT_API_REDDIT_PASSWORD, GT_API_TWITTER_KEY, GT_API_TWITTER_SECRET_KEY, GT_API_TWITTER_BEARER_TOKEN, GT_API_POLYGON_KEY, GT_FRED_API_KEY, GT_API_NEWS_TOKEN. +Alternatively, you can also set them to the following environment variables: GT_API_KEY_ALPHAVANTAGE, +GT_API_KEY_FINANCIALMODELINGPREP, GT_API_KEY_QUANDL, GT_API_REDDIT_CLIENT_ID, GT_API_REDDIT_CLIENT_SECRET, GT_API_REDDIT_USERNAME, +GT_API_REDDIT_USER_AGENT, GT_API_REDDIT_PASSWORD, GT_API_TWITTER_KEY, GT_API_TWITTER_SECRET_KEY, +GT_API_TWITTER_BEARER_TOKEN, GT_API_POLYGON_KEY, GT_FRED_API_KEY, GT_API_NEWS_TOKEN, GT_TRADIER_TOKEN . Example: ``` diff --git a/gamestonk_terminal/feature_flags.py b/gamestonk_terminal/feature_flags.py index 5c4910a1ab2..bd9c048cacc 100644 --- a/gamestonk_terminal/feature_flags.py +++ b/gamestonk_terminal/feature_flags.py @@ -11,7 +11,7 @@ USE_FLAIR = os.getenv("GTFF_USE_FLAIR") or "stars" USE_ION = strtobool(os.getenv("GTFF_USE_ION", "True")) # Enable Prompt Toolkit -USE_PROMPT_TOOLKIT = strtobool(os.getenv("GTFF_USE_PROMPT_TOOLKIT", "False")) +USE_PROMPT_TOOLKIT = True # strtobool(os.getenv("GTFF_USE_PROMPT_TOOLKIT", "False")) # Enable Prediction features ENABLE_PREDICT = strtobool(os.getenv("GTFF_ENABLE_PREDICT", "False")) From 3afd747fd45f202a3fe08e3c5365494df864ee19 Mon Sep 17 00:00:00 2001 From: Jamie Date: Fri, 16 Apr 2021 14:48:08 -0400 Subject: [PATCH 058/112] update main readme with API key --- gamestonk_terminal/feature_flags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gamestonk_terminal/feature_flags.py b/gamestonk_terminal/feature_flags.py index bd9c048cacc..5c4910a1ab2 100644 --- a/gamestonk_terminal/feature_flags.py +++ b/gamestonk_terminal/feature_flags.py @@ -11,7 +11,7 @@ USE_FLAIR = os.getenv("GTFF_USE_FLAIR") or "stars" USE_ION = strtobool(os.getenv("GTFF_USE_ION", "True")) # Enable Prompt Toolkit -USE_PROMPT_TOOLKIT = True # strtobool(os.getenv("GTFF_USE_PROMPT_TOOLKIT", "False")) +USE_PROMPT_TOOLKIT = strtobool(os.getenv("GTFF_USE_PROMPT_TOOLKIT", "False")) # Enable Prediction features ENABLE_PREDICT = strtobool(os.getenv("GTFF_ENABLE_PREDICT", "False")) From 72a2337bbbce32ecd34ec973686835b0a8ea09e6 Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 21:51:46 +0100 Subject: [PATCH 059/112] Refactor commands because OCD --- .../portfolio_optimization/optimizer_view.py | 556 ++++++++++++++---- .../portfolio_optimization/po_controller.py | 16 +- 2 files changed, 439 insertions(+), 133 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py index af081ca4708..f54ccdc3ea3 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -94,9 +94,11 @@ def equal_weight(stocks: List[str], other_args: List[str]): ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return + if len(stocks) < 2: print("Please have at least 2 loaded tickers to calculate weights.\n") return + values = {} for stock in stocks: values[stock] = ns_parser.value * round(1 / len(stocks), 5) @@ -208,38 +210,22 @@ def property_weighting(stocks: List[str], other_args: List[str]): return -def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): +def max_sharpe(stocks: List[str], other_args: List[str]): """ - Return a portfolio based on condition in port_type Defaults to 3m of historical data + Return a portfolio that maximises the Sharpe Ratio Parameters ---------- stocks: List[str] List of the stocks to be included in the weights - port_type: str - Method to be used on ef object (example: max_sharpe, min_volatility, eff_risk, eff_ret) Returns ------- weights: dict Dictionary of weights where keys are the tickers. """ - - d_descriptions = { - "max_sharpe": """Maximise the Sharpe Ratio. The result is also referred to as the tangency portfolio, - as it is the portfolio for which the capital market line is tangent to the efficient frontier. - - This is a convex optimization problem after making a certain variable substitution. See - Cornuejols and Tutuncu (2006) for more.""", - "min_volatility": "optimizes for minimum volatility", - "max_quadratic_utility": "maximises the quadratic utility, given some risk aversion", - "efficient_risk": """Maximise return for a target risk. The resulting portfolio will have - a volatility less than the target (but not guaranteed to be equal).""", - "efficient_return": "Calculate the 'Markowitz portfolio', minimising volatility for a given target return.", - } - parser = argparse.ArgumentParser( - add_help=False, prog=port_type, description=d_descriptions[port_type] + add_help=False, prog="maxsharpe", description="Maximise the Sharpe Ratio" ) parser.add_argument( "-p", @@ -265,90 +251,24 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): help="Display a pie chart for weights", ) - if port_type == "max_sharpe": - if other_args: - if "-" not in other_args[0]: - other_args.insert(0, "-r") - parser.add_argument( - "-r", - "--risk-free-rate", - type=float, - dest="risk_free_rate", - default=0.02, - help="""Risk-free rate of borrowing/lending. The period of the risk-free rate - should correspond to the frequency of expected returns.""", - ) - - elif port_type == "efficient_risk": - if other_args: - if "-" not in other_args[0]: - other_args.insert(0, "-t") - parser.add_argument( - "-t", - "--target-volatility", - type=float, - dest="target_volatility", - default=0.1, - help="The desired maximum volatility of the resulting portfolio", - ) - parser.add_argument( - "-n", - "--market-neutral", - action="store_true", - default=False, - dest="market_neutral", - help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False. - Requires negative lower weight bound.""", - ) - - elif port_type == "efficient_return": - if other_args: - if "-" not in other_args[0]: - other_args.insert(0, "-t") - parser.add_argument( - "-t", - "--target-return", - type=float, - dest="target_return", - default=0.1, - help="the desired return of the resulting portfolio", - ) - parser.add_argument( - "-n", - "--market-neutral", - action="store_true", - default=False, - dest="market_neutral", - help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False. - Requires negative lower weight bound.""", - ) - - elif port_type == "max_quadratic_utility": - if other_args: - if "-" not in other_args[0]: - other_args.insert(0, "-r") - parser.add_argument( - "-r", - "--risk-aversion", - type=float, - dest="risk_aversion", - default=1, - help="risk aversion parameter", - ) - parser.add_argument( - "-n", - "--market-neutral", - action="store_true", - default=False, - dest="market_neutral", - help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False. - Requires negative lower weight bound.""", - ) + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-r") + parser.add_argument( + "-r", + "--risk-free-rate", + type=float, + dest="risk_free_rate", + default=0.02, + help="""Risk-free rate of borrowing/lending. The period of the risk-free rate + should correspond to the frequency of expected returns.""", + ) try: ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return None + if len(stocks) < 2: print("Please have at least 2 loaded tickers to calculate weights.\n") return @@ -359,36 +279,8 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): sp = d_period[ns_parser.period] - if port_type == "max_sharpe": - ef_opt = dict(ef.max_sharpe(ns_parser.risk_free_rate)) - s_title = f"{sp} Weights that maximize Sharpe ratio with risk free level of {ns_parser.risk_free_rate}" - - elif port_type == "min_volatility": - ef_opt = dict(ef.min_volatility()) - s_title = f"{sp} Weights that minimize volatility" - - elif port_type == "efficient_risk": - ef_opt = dict( - ef.efficient_risk(ns_parser.target_volatility, ns_parser.market_neutral) - ) - s_title = f"{sp} Weights that maximize return with a maximum volatility of {ns_parser.target_volatility}" - - elif port_type == "efficient_return": - ef_opt = dict( - ef.efficient_return(ns_parser.target_return, ns_parser.market_neutral) - ) - s_title = f"{sp} Weights that minimise volatility for a given target return of {ns_parser.target_return}" - - elif port_type == "max_quadratic_utility": - ef_opt = dict( - ef.max_quadratic_utility( - ns_parser.risk_aversion, ns_parser.market_neutral - ) - ) - s_title = f"{sp} Weights that maximise the quadratic utility with risk aversion of {ns_parser.risk_aversion}" - - else: - raise ValueError("EF Method not found") + ef_opt = dict(ef.max_sharpe(ns_parser.risk_free_rate)) + s_title = f"{sp} Weights that maximize Sharpe ratio with risk free level of {ns_parser.risk_free_rate}" weights = { key: ns_parser.value * round(value, 5) for key, value in ef_opt.items() @@ -410,6 +302,414 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): return +def min_volatility(stocks: List[str], other_args: List[str]): + """ + Return a portfolio that optimizes for minimum volatility + + Parameters + ---------- + stocks: List[str] + List of the stocks to be included in the weights + + Returns + ------- + weights: dict + Dictionary of weights where keys are the tickers. + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="min_volatility", + description="Optimizes for minimum volatility", + ) + parser.add_argument( + "-p", + "--period", + default="3mo", + dest="period", + help="period to get yfinance data from", + choices=period_choices, + ) + parser.add_argument( + "-v", + "--value", + dest="value", + help="Amount to allocate to portfolio", + type=float, + default=1.0, + ) + parser.add_argument( + "--pie", + action="store_true", + dest="pie", + default=False, + help="Display a pie chart for weights", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return None + + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return + + period = ns_parser.period + stock_prices = process_stocks(stocks, period) + ef = prepare_efficient_frontier(stock_prices) + + sp = d_period[ns_parser.period] + + ef_opt = dict(ef.min_volatility()) + s_title = f"{sp} Weights that minimize volatility" + + weights = { + key: ns_parser.value * round(value, 5) for key, value in ef_opt.items() + } + + if ns_parser.pie: + pie_chart_weights(weights, s_title) + else: + print(s_title) + display_weights(weights) + print("") + + ef.portfolio_performance(verbose=True) + print("") + + except Exception as e: + print(e) + print("") + return + + +def max_quadratic_utility(stocks: List[str], other_args: List[str]): + """ + Return a portfolio that maximises the quadratic utility, given some risk aversion + + Parameters + ---------- + stocks: List[str] + List of the stocks to be included in the weights + + Returns + ------- + weights: dict + Dictionary of weights where keys are the tickers. + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="max_quadratic_utility", + description="Maximises the quadratic utility, given some risk aversion", + ) + parser.add_argument( + "-p", + "--period", + default="3mo", + dest="period", + help="period to get yfinance data from", + choices=period_choices, + ) + parser.add_argument( + "-v", + "--value", + dest="value", + help="Amount to allocate to portfolio", + type=float, + default=1.0, + ) + parser.add_argument( + "-n", + "--market-neutral", + action="store_true", + default=False, + dest="market_neutral", + help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False. + Requires negative lower weight bound.""", + ) + if "-n" not in other_args: + parser.add_argument( + "--pie", + action="store_true", + dest="pie", + default=False, + help="Display a pie chart for weights. Only if neutral flag is left False.", + ) + + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-r") + + parser.add_argument( + "-r", + "--risk-aversion", + type=float, + dest="risk_aversion", + default=1, + help="risk aversion parameter", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return None + + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return + + period = ns_parser.period + stock_prices = process_stocks(stocks, period) + ef = prepare_efficient_frontier(stock_prices) + + sp = d_period[ns_parser.period] + + ef_opt = dict( + ef.max_quadratic_utility(ns_parser.risk_aversion, ns_parser.market_neutral) + ) + s_title = f"{sp} Weights that maximise the quadratic utility with risk aversion of {ns_parser.risk_aversion}" + + weights = { + key: ns_parser.value * round(value, 5) for key, value in ef_opt.items() + } + + if not ns_parser.market_neutral: + if ns_parser.pie: + pie_chart_weights(weights, s_title) + ef.portfolio_performance(verbose=True) + print("") + return + + print(s_title) + display_weights(weights) + print("") + ef.portfolio_performance(verbose=True) + print("") + + except Exception as e: + print(e) + print("") + return + + +def efficient_risk(stocks: List[str], other_args: List[str]): + """ + Return a portfolio that maximises return for a target risk. The resulting portfolio will have + a volatility less than the target (but not guaranteed to be equal) + + Parameters + ---------- + stocks: List[str] + List of the stocks to be included in the weights + + Returns + ------- + weights: dict + Dictionary of weights where keys are the tickers. + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="effrisk", + description="""Maximise return for a target risk. The resulting portfolio will have + a volatility less than the target (but not guaranteed to be equal)""", + ) + parser.add_argument( + "-p", + "--period", + default="3mo", + dest="period", + help="period to get yfinance data from", + choices=period_choices, + ) + parser.add_argument( + "-v", + "--value", + dest="value", + help="Amount to allocate to portfolio", + type=float, + default=1.0, + ) + parser.add_argument( + "-n", + "--market-neutral", + action="store_true", + default=False, + dest="market_neutral", + help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False. + Requires negative lower weight bound.""", + ) + if "-n" not in other_args: + parser.add_argument( + "--pie", + action="store_true", + dest="pie", + default=False, + help="Display a pie chart for weights. Only if neutral flag is left False.", + ) + + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-t") + parser.add_argument( + "-t", + "--target-volatility", + type=float, + dest="target_volatility", + default=0.1, + help="The desired maximum volatility of the resulting portfolio", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return None + + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return + + period = ns_parser.period + stock_prices = process_stocks(stocks, period) + ef = prepare_efficient_frontier(stock_prices) + + sp = d_period[ns_parser.period] + + ef_opt = dict( + ef.efficient_risk(ns_parser.target_volatility, ns_parser.market_neutral) + ) + s_title = f"{sp} Weights that maximize return with a maximum volatility of {ns_parser.target_volatility}" + + weights = { + key: ns_parser.value * round(value, 5) for key, value in ef_opt.items() + } + + if not ns_parser.market_neutral: + if ns_parser.pie: + pie_chart_weights(weights, s_title) + ef.portfolio_performance(verbose=True) + print("") + return + + print(s_title) + display_weights(weights) + print("") + ef.portfolio_performance(verbose=True) + print("") + + except Exception as e: + print(e) + print("") + return + + +def efficient_return(stocks: List[str], other_args: List[str]): + """ + Return a portfolio that minimises volatility for a given target return ('Markowitz portfolio') + + Parameters + ---------- + stocks: List[str] + List of the stocks to be included in the weights + + Returns + ------- + weights: dict + Dictionary of weights where keys are the tickers. + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="effret", + description="Calculate the 'Markowitz portfolio', minimising volatility for a given target return", + ) + parser.add_argument( + "-p", + "--period", + default="3mo", + dest="period", + help="period to get yfinance data from", + choices=period_choices, + ) + parser.add_argument( + "-v", + "--value", + dest="value", + help="Amount to allocate to portfolio", + type=float, + default=1.0, + ) + parser.add_argument( + "-n", + "--market-neutral", + action="store_true", + default=False, + dest="market_neutral", + help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False. + Requires negative lower weight bound.""", + ) + if "-n" not in other_args: + parser.add_argument( + "--pie", + action="store_true", + dest="pie", + default=False, + help="Display a pie chart for weights. Only if neutral flag is left False.", + ) + + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-t") + parser.add_argument( + "-t", + "--target-return", + type=float, + dest="target_return", + default=0.1, + help="the desired return of the resulting portfolio", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return None + + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return + + period = ns_parser.period + stock_prices = process_stocks(stocks, period) + ef = prepare_efficient_frontier(stock_prices) + + sp = d_period[ns_parser.period] + + ef_opt = dict( + ef.efficient_return(ns_parser.target_return, ns_parser.market_neutral) + ) + s_title = f"{sp} Weights that minimise volatility for a given target return of {ns_parser.target_return}" + + weights = { + key: ns_parser.value * round(value, 5) for key, value in ef_opt.items() + } + + if not ns_parser.market_neutral: + if ns_parser.pie: + pie_chart_weights(weights, s_title) + ef.portfolio_performance(verbose=True) + print("") + return + + print(s_title) + display_weights(weights) + print("") + ef.portfolio_performance(verbose=True) + print("") + + except Exception as e: + print(e) + print("") + return + + def show_ef(stocks: List[str], other_args: List[str]): parser = argparse.ArgumentParser( add_help=False, diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index a7ef204f128..398cbc883ab 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -5,11 +5,13 @@ __docformat__ = "numpy" import argparse from typing import List import matplotlib.pyplot as plt +from datetime import datetime from prompt_toolkit.completion import NestedCompleter from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import get_flair, parse_known_args_and_warn from gamestonk_terminal.menu import session from gamestonk_terminal.portfolio_optimization import optimizer_view +from gamestonk_terminal.comparison_analysis import ca_controller class PortfolioOptimization: @@ -28,6 +30,7 @@ class PortfolioOptimization: "effrisk", "maxquadutil", "ef", + "ca", "yolo", ] @@ -74,6 +77,9 @@ class PortfolioOptimization: print("") print(" ef show the efficient frontier") print("") + if tickers: + print(" > ca comparison analysis for selected tickers") + print("") def switch(self, an_input: str): """Process and dispatch input @@ -122,23 +128,23 @@ class PortfolioOptimization: def call_maxsharpe(self, other_args: List[str]): """Process maxsharpe command""" - optimizer_view.ef_portfolio(self.tickers, "max_sharpe", other_args) + optimizer_view.max_sharpe(self.tickers, other_args) def call_minvol(self, other_args: List[str]): """Process minvol command""" - optimizer_view.ef_portfolio(self.tickers, "min_volatility", other_args) + optimizer_view.min_volatility(self.tickers, other_args) def call_maxquadutil(self, other_args: List[str]): """Process maxquadutil command""" - optimizer_view.ef_portfolio(self.tickers, "max_quadratic_utility", other_args) + optimizer_view.max_quadratic_utility(self.tickers, other_args) def call_effrisk(self, other_args: List[str]): """Process effrisk command""" - optimizer_view.ef_portfolio(self.tickers, "efficient_risk", other_args) + optimizer_view.efficient_risk(self.tickers, other_args) def call_effret(self, other_args: List[str]): """Process effret command""" - optimizer_view.ef_portfolio(self.tickers, "efficient_return", other_args) + optimizer_view.efficient_return(self.tickers, other_args) def call_ef(self, other_args): """Process ef command""" From 26923bfa07bf7245542af9948f6dd3a86b1c04e2 Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 22:01:19 +0100 Subject: [PATCH 060/112] Add dividend and mktcap back as commands. Fix not finding property different than None --- .../portfolio_optimization/optimizer_view.py | 6 ++++++ .../portfolio_optimization/po_controller.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py index f54ccdc3ea3..6c61666d5bf 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -193,6 +193,12 @@ def property_weighting(stocks: List[str], other_args: List[str]): stock_prop = 0 prop[stock] = stock_prop prop_sum += stock_prop + + if prop_sum == 0: + print(f"No {ns_parser.property} was found on list of tickers provided") + print("") + return + for k, v in prop.items(): weights[k] = round(v / prop_sum, 5) * ns_parser.value diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index 398cbc883ab..98fb0cf4557 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -23,6 +23,8 @@ class PortfolioOptimization: "select", "add", "equal", + "mktcap", + "dividend", "property", "maxsharpe", "minvol", @@ -60,6 +62,10 @@ class PortfolioOptimization: print("") print("Optimization:") print(" equal equally weighted") + print(" mktcap weighted according to market cap (property marketCap)") + print( + " dividend weighted according to dividend yield (property dividendYield)" + ) print( " property weight according to selected info property (e.g. marketCap)" ) @@ -122,6 +128,16 @@ class PortfolioOptimization: """Process equal command""" optimizer_view.equal_weight(self.tickers, other_args) + def call_mktcap(self, other_args: List[str]): + """Process mktcap command""" + other_args.insert(0, "marketCap") + optimizer_view.property_weighting(self.tickers, other_args) + + def call_dividend(self, other_args: List[str]): + """Process dividend command""" + other_args.insert(0, "dividendYield") + optimizer_view.property_weighting(self.tickers, other_args) + def call_property(self, other_args: List[str]): """Process property command""" optimizer_view.property_weighting(self.tickers, other_args) From c8a5fa81deed66f0eef5414ed85510a591650af5 Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 22:03:38 +0100 Subject: [PATCH 061/112] Refactor po_controller helper --- gamestonk_terminal/portfolio_optimization/po_controller.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index 98fb0cf4557..0913f2b50d4 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -66,9 +66,7 @@ class PortfolioOptimization: print( " dividend weighted according to dividend yield (property dividendYield)" ) - print( - " property weight according to selected info property (e.g. marketCap)" - ) + print(" property weight according to selected info property") print("") print("Mean Variance Optimization:") print( From 6b85a2bcd38477d8fdccf1573b4ee3a857755a66 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 22:12:18 +0100 Subject: [PATCH 062/112] Update README.md --- .../portfolio_optimization/README.md | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md index d3591364c21..6542f2476c5 100644 --- a/gamestonk_terminal/portfolio_optimization/README.md +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -8,8 +8,12 @@ This menu aims to optimize a portfolio of pre-loaded stocks, and the usage of th * overwrite current tickers with new tickers * [equal](#equal) * equally weighted +* [mktcap](#mktcap) + * weighted according to market cap (property marketCap) +* [dividend](#dividend) + * weighted according to dividend yield (property dividendYield) * [property](#property) - * weight according to selected info property (e.g. marketCap) + * weight according to selected info property [Mean Variance Optimization](#Mean_Variance_Optimization) @@ -63,6 +67,34 @@ Returns an equally weighted portfolio ![equal](https://user-images.githubusercontent.com/25267873/114958292-9771e080-9e5a-11eb-9de6-1c2dfbe1e27b.png) +### mktcap + +```text +property -p marketCap [-v VALUE] [--pie] +``` + +Returns a portfolio that is weighted based on market cap info + +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights + +![mktCap](https://user-images.githubusercontent.com/25267873/115084151-57fdcf80-9f00-11eb-885c-bfe93de0eb6b.png) + + +### dividend + +```text +property -p dividend [-v VALUE] [--pie] +``` + +Returns a portfolio that is weighted based on dividend yield info + +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights + +Captura de ecrã 2021-04-16, às 22 11 41 + + ### property ```text From f97a2eea1d9de103db4a7790c732d0ac4b437f5a Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 22:19:23 +0100 Subject: [PATCH 063/112] Update README.md --- gamestonk_terminal/portfolio_optimization/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md index 6542f2476c5..3e9e44d4603 100644 --- a/gamestonk_terminal/portfolio_optimization/README.md +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -114,9 +114,9 @@ Returns a portfolio that is weighted based on a selected property info These approaches are based off of the efficient frontier approach, which is meant to solve the following optimization problem. -Minimize: - -Subject to: , and +

+ +

* 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). From 17416405332b6eef9b4328833b27cba9ede6ea9b Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 22:42:36 +0100 Subject: [PATCH 064/112] Improve datetime shown in trending/latest disc news --- gamestonk_terminal/discovery/seeking_alpha_model.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index 7e85ca8ea70..a4857827a0a 100755 --- a/gamestonk_terminal/discovery/seeking_alpha_model.py +++ b/gamestonk_terminal/discovery/seeking_alpha_model.py @@ -6,6 +6,7 @@ import requests from bs4 import BeautifulSoup import pandas as pd from pandas.core.frame import DataFrame +from datetime import datetime from gamestonk_terminal.helper_funcs import get_user_agent @@ -121,10 +122,15 @@ def get_article_list(num: int) -> List[dict]: if not article_url.startswith("/news/"): continue article_id = article_url.split("/")[2].split("-")[0] + articles.append( { "title": item.text, - "publishedAt": item_row["data-last-date"], + "publishedAt": datetime.strptime( + item_row["data-last-date"], "%Y-%m-%d %H:%M:%S %z" + ) + .time() + .strftime("%Y-%m-%d %H:%M:%S"), "url": "https://seekingalpha.com" + article_url, "id": article_id, } @@ -166,7 +172,7 @@ def get_trending_list(num: int) -> list: articles.append( { "title": item["title"], - "publishedAt": item["publish_on"], + "publishedAt": item["publish_on"][: item["publish_on"].rfind(".")], "url": "https://seekingalpha.com" + article_url, "id": article_id, } From 1bba88b2e671ebd21bd74fd5a080e3b9879cc18d Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 23:10:50 +0100 Subject: [PATCH 065/112] refactor to allow to access articles from latest/trending commands --- .../discovery/disc_controller.py | 6 - .../discovery/seeking_alpha_view.py | 140 ++++++++++-------- 2 files changed, 78 insertions(+), 68 deletions(-) diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index a9a66116107..d74eaf19814 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -48,7 +48,6 @@ class DiscoveryController: "spectrum", "latest", "trending", - "news_article", ] def __init__(self): @@ -92,7 +91,6 @@ class DiscoveryController: print(" spectrum spectrum of sectors, industry, country [Finviz]") print(" latest latest news [SeekingAlpha]") print(" trending trending news [SeekingAlpha]") - print(" news_article news article [SeekingAlpha]") print("") def switch(self, an_input: str): @@ -201,10 +199,6 @@ class DiscoveryController: """Process trending command""" seeking_alpha_view.trending_news_view(other_args) - def call_news_article(self, other_args: List[str]): - """Process news_article command""" - seeking_alpha_view.news_article_view(other_args) - def menu(): """Discovery Menu""" diff --git a/gamestonk_terminal/discovery/seeking_alpha_view.py b/gamestonk_terminal/discovery/seeking_alpha_view.py index 639e145f0d0..a11c548be72 100644 --- a/gamestonk_terminal/discovery/seeking_alpha_view.py +++ b/gamestonk_terminal/discovery/seeking_alpha_view.py @@ -73,15 +73,22 @@ def latest_news_view(other_args: List[str]): Parameters ---------- other_args : List[str] - argparse other args - ["-n", "5"] + argparse other args - ["-i", "123123", "-n", "5"] """ - parser = argparse.ArgumentParser( add_help=False, prog="latest", description="""Latest news articles. [Source: Seeking Alpha]""", ) - + parser.add_argument( + "-i", + "--id", + action="store", + dest="n_id", + type=check_positive, + default=-1, + help="article number found on Seeking Alpha website", + ) parser.add_argument( "-n", "--num", @@ -92,24 +99,44 @@ def latest_news_view(other_args: List[str]): help="number of latest articles being printed", ) + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-i") + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return - articles = seeking_alpha_model.get_article_list(ns_parser.n_num) - for idx, article in enumerate(articles): + # User wants to see all latest news + if ns_parser.n_id == -1: + articles = seeking_alpha_model.get_article_list(ns_parser.n_num) + for idx, article in enumerate(articles): + print( + article["publishedAt"].replace("T", " ").replace("Z", ""), + "-", + article["id"], + "-", + article["title"], + ) + print(article["url"]) + print("") + + if idx >= ns_parser.n_num - 1: + break + + # User wants to access specific article + else: + article = seeking_alpha_model.get_article_data(ns_parser.n_id) print( - article["publishedAt"].replace("T", " ").replace("Z", ""), - "-", - article["id"], - "-", + article["publishedAt"][: article["publishedAt"].rfind(":") - 3].replace( + "T", " " + ), + " ", article["title"], ) print(article["url"]) print("") - - if idx >= ns_parser.n_num - 1: - break + print(article["content"]) def trending_news_view(other_args: List[str]): @@ -118,15 +145,22 @@ def trending_news_view(other_args: List[str]): Parameters ---------- other_args : List[str] - argparse other args - ["-n", "5"] + argparse other args - ["i", "123123", "-n", "5"] """ - parser = argparse.ArgumentParser( add_help=False, prog="trending", description="""Trending news articles. [Source: Seeking Alpha]""", ) - + parser.add_argument( + "-i", + "--id", + action="store", + dest="n_id", + type=check_positive, + default=-1, + help="article number found on Seeking Alpha website", + ) parser.add_argument( "-n", "--num", @@ -137,59 +171,41 @@ def trending_news_view(other_args: List[str]): help="number of trending articles being printed", ) + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-i") + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return - articles = seeking_alpha_model.get_trending_list(ns_parser.n_num) - for idx, article in enumerate(articles): + # User wants to see all trending articles + if ns_parser.n_id == -1: + articles = seeking_alpha_model.get_trending_list(ns_parser.n_num) + for idx, article in enumerate(articles): + print( + article["publishedAt"].replace("T", " ").replace("Z", ""), + "-", + article["id"], + "-", + article["title"], + ) + print(article["url"]) + print("") + + if idx >= ns_parser.n_num - 1: + break + + # User wants to access specific article + else: + article = seeking_alpha_model.get_article_data(ns_parser.n_id) print( - article["publishedAt"].replace("T", " ").replace("Z", ""), - "-", - article["id"], - "-", + article["publishedAt"][: article["publishedAt"].rfind(":") - 3].replace( + "T", " " + ), + " ", article["title"], ) print(article["url"]) print("") - - if idx >= ns_parser.n_num - 1: - break - - -def news_article_view(other_args: List[str]): - """Prints a news article - - Parameters - ---------- - other_args : List[str] - argparse other args - ["-i", "3681944"] - """ - - parser = argparse.ArgumentParser( - add_help=False, - prog="up_article", - description="""News article. [Source: Seeking Alpha]""", - ) - - parser.add_argument( - "-i", - "--id", - action="store", - dest="n_id", - type=check_positive, - default=-1, - help="article number found on Seeking Alpha website", - ) - - ns_parser = parse_known_args_and_warn(parser, other_args) - if not ns_parser: - return - if ns_parser.n_id == -1: - return - - article = seeking_alpha_model.get_article_data(ns_parser.n_id) - print(article["publishedAt"], " ", article["title"]) - print(article["url"]) - print("") - print(article["content"]) + print(article["content"]) From d3b17532f0be873e013d2c8f5cd9915512263e1c Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 23:14:57 +0100 Subject: [PATCH 066/112] minor typo --- gamestonk_terminal/discovery/seeking_alpha_model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index a4857827a0a..3f056b22a2e 100755 --- a/gamestonk_terminal/discovery/seeking_alpha_model.py +++ b/gamestonk_terminal/discovery/seeking_alpha_model.py @@ -129,7 +129,6 @@ def get_article_list(num: int) -> List[dict]: "publishedAt": datetime.strptime( item_row["data-last-date"], "%Y-%m-%d %H:%M:%S %z" ) - .time() .strftime("%Y-%m-%d %H:%M:%S"), "url": "https://seekingalpha.com" + article_url, "id": article_id, From 1c8994abe9a48fc08ef2513ecb8fd2c979235e69 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 23:22:56 +0100 Subject: [PATCH 067/112] Update README.md --- gamestonk_terminal/discovery/README.md | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/gamestonk_terminal/discovery/README.md b/gamestonk_terminal/discovery/README.md index 517d142d334..2e69e69f056 100644 --- a/gamestonk_terminal/discovery/README.md +++ b/gamestonk_terminal/discovery/README.md @@ -32,6 +32,10 @@ This menu aims to discover new stocks, and the usage of the following commands a * performance of sectors, industry, country [Finviz] * [spectrum](#spectrum) * spectrum of sectors, industry, country [Finviz] +* [latest](#latest) + * latest news [Seeking Alpha] +* [trending](#trending) + * trending news [Seeking Alpha] ## map @@ -218,3 +222,30 @@ Spectrum of sectors, industry, country. [Source: Finviz] * -g : Data group (sector, industry or country). Default: Sector. ![cntry_spec](https://user-images.githubusercontent.com/25267873/113639067-48fd5e80-9670-11eb-95cf-0931845ddd12.png) + + +## latest + +``` +usage: latest [-i N_ID] [-n N_NUM] +``` + +Latest news articles. [Source: Seeking Alpha] +* -i : article number found on Seeking Alpha website +* -n : number of latest articles being printed. Default 10. + +latest + + +## trending + +``` +usage: trending [-i N_ID] [-n N_NUM] +``` + +Trending news articles. [Source: Seeking Alpha] +* -i : article number found on Seeking Alpha website +* -n : number of trending articles being printed. Default 10. + +trending + From bd906bcb2375538080c04540074687e1daea6681 Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 23:27:26 +0100 Subject: [PATCH 068/112] spacing --- gamestonk_terminal/discovery/disc_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index d74eaf19814..ac0ac589526 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -89,8 +89,8 @@ class DiscoveryController: print(" valuation valuation of sectors, industry, country [Finviz]") print(" performance performance of sectors, industry, country [Finviz]") print(" spectrum spectrum of sectors, industry, country [Finviz]") - print(" latest latest news [SeekingAlpha]") - print(" trending trending news [SeekingAlpha]") + print(" latest latest news [Seeking Alpha]") + print(" trending trending news [Seeking Alpha]") print("") def switch(self, an_input: str): From 96b15688759675fbd11b75c79529757c4fab2602 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 23:28:56 +0100 Subject: [PATCH 069/112] Update README.md --- gamestonk_terminal/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 2b6d79c7b2e..09b25108e07 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -79,6 +79,8 @@ Command|Description|Source `valuation` |valuation of sectors, industry, country |[Finviz](https://finviz.com) `performance` |performance of sectors, industry, country |[Finviz](https://finviz.com) `spectrum` |spectrum of sectors, industry, country |[Finviz](https://finviz.com) +`latest` |latest news |[Seeking Alpha](https://seekingalpha.com/) +`trending` |trending news |[Seeking Alpha](https://seekingalpha.com/)   @@ -351,4 +353,4 @@ technical |technical (e.g. Beta, SMA50, 52W Low, RSI, Change) signals |view filter signals (e.g. -s top_gainers) -  \ No newline at end of file +  From ce6c369968d0208c719b3b143acf495cde391164 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 23:31:42 +0100 Subject: [PATCH 070/112] Update ROADMAP.md --- ROADMAP.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ROADMAP.md b/ROADMAP.md index f5c466f3a9a..df45ed9d191 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -33,6 +33,7 @@ * [x] Top Losers (@didier) - [PR #171](https://github.com/DidierRLopes/GamestonkTerminal/pull/171) * [x] ARK orders (@aia) - [PR #140](https://github.com/DidierRLopes/GamestonkTerminal/pull/140) * [x] Analyse sectors/industry performance, overview and valuation from Finviz (@didier) - [PR #315](https://github.com/DidierRLopes/GamestonkTerminal/pull/315) +* [x] Add latest and trending news commands (@hinxx) - [PR #347](https://github.com/DidierRLopes/GamestonkTerminal/pull/347) **NEXT** From 27aa3251b03279b37464ebee1ba7c2258e853b40 Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 23:32:07 +0100 Subject: [PATCH 071/112] make lint happy --- gamestonk_terminal/discovery/seeking_alpha_model.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index 3f056b22a2e..b573c506d3f 100755 --- a/gamestonk_terminal/discovery/seeking_alpha_model.py +++ b/gamestonk_terminal/discovery/seeking_alpha_model.py @@ -128,8 +128,7 @@ def get_article_list(num: int) -> List[dict]: "title": item.text, "publishedAt": datetime.strptime( item_row["data-last-date"], "%Y-%m-%d %H:%M:%S %z" - ) - .strftime("%Y-%m-%d %H:%M:%S"), + ).strftime("%Y-%m-%d %H:%M:%S"), "url": "https://seekingalpha.com" + article_url, "id": article_id, } From 4a7b7402368693e39ce2167fe03c4977be4dc881 Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 23:35:32 +0100 Subject: [PATCH 072/112] flake8 leave me alone pleaaaase --- gamestonk_terminal/comparison_analysis/ca_controller.py | 1 - gamestonk_terminal/portfolio_optimization/po_controller.py | 1 - 2 files changed, 2 deletions(-) diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py index 499b6c9cd39..deedd6b8ff6 100644 --- a/gamestonk_terminal/comparison_analysis/ca_controller.py +++ b/gamestonk_terminal/comparison_analysis/ca_controller.py @@ -5,7 +5,6 @@ import argparse import requests import random from typing import List -from datetime import datetime import pandas as pd from matplotlib import pyplot as plt from gamestonk_terminal import feature_flags as gtff diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index 0913f2b50d4..6f2bb6b7bda 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -5,7 +5,6 @@ __docformat__ = "numpy" import argparse from typing import List import matplotlib.pyplot as plt -from datetime import datetime from prompt_toolkit.completion import NestedCompleter from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import get_flair, parse_known_args_and_warn From f8bd6f9e6ac1651992a146ea0c6044f40972d442 Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 23:39:10 +0100 Subject: [PATCH 073/112] removed wrong import --- gamestonk_terminal/comparison_analysis/ca_controller.py | 1 + gamestonk_terminal/portfolio_optimization/po_controller.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py index deedd6b8ff6..499b6c9cd39 100644 --- a/gamestonk_terminal/comparison_analysis/ca_controller.py +++ b/gamestonk_terminal/comparison_analysis/ca_controller.py @@ -5,6 +5,7 @@ import argparse import requests import random from typing import List +from datetime import datetime import pandas as pd from matplotlib import pyplot as plt from gamestonk_terminal import feature_flags as gtff diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index 6f2bb6b7bda..c66ca63e846 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 -from gamestonk_terminal.comparison_analysis import ca_controller class PortfolioOptimization: From 1daa6f8efbfc9f2b231eace3252de2a5ed93dace Mon Sep 17 00:00:00 2001 From: didier Date: Sat, 17 Apr 2021 01:13:35 +0100 Subject: [PATCH 074/112] Improve user experience on disc/ratings command --- .../discovery/disc_controller.py | 2 +- .../discovery/marketbeat_view.py | 100 ++++++++++++++---- 2 files changed, 82 insertions(+), 20 deletions(-) diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index de17f94334a..6125a377773 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -89,7 +89,7 @@ class DiscoveryController: print(" valuation valuation of sectors, industry, country [Finviz]") print(" performance performance of sectors, industry, country [Finviz]") print(" spectrum spectrum of sectors, industry, country [Finviz]") - print(" ratings latest ratings [MarketBeat]") + print(" ratings top ratings updates [MarketBeat]") print("") def switch(self, an_input: str): diff --git a/gamestonk_terminal/discovery/marketbeat_view.py b/gamestonk_terminal/discovery/marketbeat_view.py index aad9b03ec5c..2a55f7c3479 100644 --- a/gamestonk_terminal/discovery/marketbeat_view.py +++ b/gamestonk_terminal/discovery/marketbeat_view.py @@ -2,6 +2,8 @@ __docformat__ = "numpy" import argparse +import numpy as np +import pandas as pd from typing import List from gamestonk_terminal.helper_funcs import ( check_positive, @@ -17,38 +19,98 @@ def ratings_view(other_args: List[str]): Parameters ---------- other_args : List[str] - argparse other args - ["-n", "5"] + argparse other args - ["-t", "100"] """ - parser = argparse.ArgumentParser( add_help=False, prog="ratings", description="""Latest ratings. [Source: MarketBeat]""", ) - parser.add_argument( - "-n", - "--num", + "-t", + "--threshold", action="store", - dest="n_num", + dest="n_threshold", type=check_positive, - default=10, - help="number of ratings to print", + default=100, + help="threshold to show latest ratings that show a drastic increase", ) + 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 - ratings = marketbeat_model.get_ratings() - for idx, rating in enumerate(ratings): - print( - rating["ticker"], - rating["action"].replace(" by", ""), - rating["impact"], - rating["current_price"], - "->", - rating["target_price"], + try: + ratings = marketbeat_model.get_ratings() + + df_ratings = pd.DataFrame(ratings) + df_ratings = df_ratings[df_ratings["target_price"] != ""] + df_ratings["old_target"] = df_ratings["target_price"].apply( + lambda x: x.split(" ➝ ")[0] if "➝" in x else "" ) - if idx >= ns_parser.n_num - 1: - break + df_ratings["new_target"] = df_ratings["target_price"].apply( + lambda x: x.split(" ➝ ")[1] if "➝" in x else x + ) + + df_ratings["clean_current_price"] = df_ratings["current_price"].apply( + lambda x: "".join([c if c.isdigit() or c == "." else "" for c in str(x)]) + ) + df_ratings["clean_current_price"] = df_ratings["clean_current_price"].astype( + float + ) + df_ratings["clean_target_price"] = df_ratings["new_target"].apply( + lambda x: "".join([c if c.isdigit() or c == "." else "" for c in str(x)]) + ) + df_ratings["clean_target_price"] = df_ratings["clean_target_price"].astype( + float + ) + + df_ratings["pct_increase"] = ( + 100 + * (df_ratings["clean_target_price"] - df_ratings["clean_current_price"]) + / df_ratings["clean_current_price"] + ) + df_ratings["pct_increase"] = df_ratings["pct_increase"].round(2) + + df_ratings["pct_abs"] = np.abs(df_ratings["pct_increase"]) + df_ratings["pct_abs"] = df_ratings["pct_abs"].round(2) + + df_ratings = df_ratings.sort_values("pct_abs", ascending=False) + + df_ratings_top = df_ratings[df_ratings["pct_abs"] > ns_parser.n_threshold][ + [ + "ticker", + "brokerage", + "rating", + "current_price", + "old_target", + "new_target", + "pct_increase", + ] + ] + + df_ratings_top["pct_increase"] = df_ratings_top["pct_increase"].apply( + lambda x: str(x) + " %" + ) + + print( + df_ratings_top.rename( + columns={ + "ticker": "Ticker", + "brokerage": "Brokerage", + "rating": "Rating", + "current_price": "Current Price", + "old_target": "Old Target", + "new_target": "New Target", + "pct_increase": "Percentage Increase", + } + ).to_string(index=False) + ) + + print("") + except Exception as e: + print(e, "\n") From 6c51ecfc72cb395af3171c56002adc3ce8ec4fb0 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sat, 17 Apr 2021 01:22:01 +0100 Subject: [PATCH 075/112] Update README.md --- gamestonk_terminal/discovery/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/gamestonk_terminal/discovery/README.md b/gamestonk_terminal/discovery/README.md index 517d142d334..d1920d1975d 100644 --- a/gamestonk_terminal/discovery/README.md +++ b/gamestonk_terminal/discovery/README.md @@ -32,6 +32,8 @@ This menu aims to discover new stocks, and the usage of the following commands a * performance of sectors, industry, country [Finviz] * [spectrum](#spectrum) * spectrum of sectors, industry, country [Finviz] +* [ratings](#ratings) + * top ratings updates [MarketBeat] ## map @@ -218,3 +220,18 @@ Spectrum of sectors, industry, country. [Source: Finviz] * -g : Data group (sector, industry or country). Default: Sector. ![cntry_spec](https://user-images.githubusercontent.com/25267873/113639067-48fd5e80-9670-11eb-95cf-0931845ddd12.png) + + +## ratings + +``` +usage: ratings [-t N_THRESHOLD] +``` + +Top ratings updates. [Source: MarketBeat] + +* -t : Minimum threshold in percentage change between current and target price to show ratings. Default: 100. + +ratings + + From 134ea5794b293baf13476d9b7835849fbf9e6533 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sat, 17 Apr 2021 01:23:11 +0100 Subject: [PATCH 076/112] Update README.md --- gamestonk_terminal/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 2b6d79c7b2e..e41212591eb 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -79,6 +79,7 @@ Command|Description|Source `valuation` |valuation of sectors, industry, country |[Finviz](https://finviz.com) `performance` |performance of sectors, industry, country |[Finviz](https://finviz.com) `spectrum` |spectrum of sectors, industry, country |[Finviz](https://finviz.com) +`ratings ` |top ratings updates |[MarketBeat](https://marketbeat.com)   @@ -351,4 +352,4 @@ technical |technical (e.g. Beta, SMA50, 52W Low, RSI, Change) signals |view filter signals (e.g. -s top_gainers) -  \ No newline at end of file +  From b430f11c87a441bfe653b9bb3242794522b37710 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sat, 17 Apr 2021 01:25:35 +0100 Subject: [PATCH 077/112] Update ROADMAP.md --- ROADMAP.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ROADMAP.md b/ROADMAP.md index f5c466f3a9a..86e76c6d2ac 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -33,6 +33,7 @@ * [x] Top Losers (@didier) - [PR #171](https://github.com/DidierRLopes/GamestonkTerminal/pull/171) * [x] ARK orders (@aia) - [PR #140](https://github.com/DidierRLopes/GamestonkTerminal/pull/140) * [x] Analyse sectors/industry performance, overview and valuation from Finviz (@didier) - [PR #315](https://github.com/DidierRLopes/GamestonkTerminal/pull/315) +* [x] Add top ratings updates (@hinxx) - [PR #350](https://github.com/DidierRLopes/GamestonkTerminal/pull/350) **NEXT** From a19db810ed4e4c139073e75f939c3d15c0719bbe Mon Sep 17 00:00:00 2001 From: didier Date: Sat, 17 Apr 2021 01:31:08 +0100 Subject: [PATCH 078/112] improve description of top ratings command --- gamestonk_terminal/discovery/marketbeat_view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gamestonk_terminal/discovery/marketbeat_view.py b/gamestonk_terminal/discovery/marketbeat_view.py index 2a55f7c3479..13ec048ee5c 100644 --- a/gamestonk_terminal/discovery/marketbeat_view.py +++ b/gamestonk_terminal/discovery/marketbeat_view.py @@ -14,7 +14,7 @@ from gamestonk_terminal.discovery import marketbeat_model def ratings_view(other_args: List[str]): - """Prints a list with ratings + """Prints top ratings updates Parameters ---------- @@ -24,7 +24,7 @@ def ratings_view(other_args: List[str]): parser = argparse.ArgumentParser( add_help=False, prog="ratings", - description="""Latest ratings. [Source: MarketBeat]""", + description="""Top ratings updates. [Source: MarketBeat]""", ) parser.add_argument( "-t", @@ -33,7 +33,7 @@ def ratings_view(other_args: List[str]): dest="n_threshold", type=check_positive, default=100, - help="threshold to show latest ratings that show a drastic increase", + help="Minimum threshold in percentage change between current and target price to show ratings", ) if other_args: From d78f4d176b5b75826026d19eb4f677be63e06b88 Mon Sep 17 00:00:00 2001 From: didier Date: Sat, 17 Apr 2021 15:55:05 +0100 Subject: [PATCH 079/112] fix weird indentation from git resolve conflicts --- gamestonk_terminal/discovery/disc_controller.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index 9b5903ccc63..2362e8f762b 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -194,15 +194,15 @@ class DiscoveryController: other_args, "spectrum" ) - def call_latest(self, other_args: List[str]): + def call_latest(self, other_args: List[str]): """Process latest command""" seeking_alpha_view.latest_news_view(other_args) def call_trending(self, other_args: List[str]): """Process trending command""" seeking_alpha_view.trending_news_view(other_args) - - def call_ratings(self, other_args: List[str]): + + def call_ratings(self, other_args: List[str]): """Process ratings command""" marketbeat_view.ratings_view(other_args) From cf12d791cce590f3f15ab16fc394952fc8dc5ec3 Mon Sep 17 00:00:00 2001 From: didier Date: Sat, 17 Apr 2021 17:57:23 +0100 Subject: [PATCH 080/112] address statsmodels on poetry #295 --- poetry.lock | 10 +++++++--- pyproject.toml | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5cbefaf1f5d..87eb1a904e2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1643,7 +1643,7 @@ name = "patsy" version = "0.5.1" description = "A Python package for describing statistical models and for building design matrices." category = "main" -optional = true +optional = false python-versions = "*" [package.dependencies] @@ -2441,7 +2441,7 @@ name = "statsmodels" version = "0.12.2" description = "Statistical computations and models for Python" category = "main" -optional = true +optional = false python-versions = ">=3.6" [package.dependencies] @@ -2918,7 +2918,7 @@ prediction = ["fbprophet", "tensorflow", "pmdarima", "transformers", "flair", "t [metadata] lock-version = "1.1" python-versions = "^3.6.8" -content-hash = "5b3abe79834fc75092d1d9e8e3a809252f56eddc170fb61a93c36ffef9328b73" +content-hash = "b328b5fa451201792194e1919c729e3e0105b613ad2900405f65280f6afdb3aa" [metadata.files] absl-py = [ @@ -4523,6 +4523,7 @@ 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"}, @@ -4530,6 +4531,7 @@ 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"}, @@ -4537,6 +4539,7 @@ 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"}, @@ -4544,6 +4547,7 @@ 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"}, ] diff --git a/pyproject.toml b/pyproject.toml index 1082086197e..17ae81736e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ tradingview-ta = "^3.2.3" finvizfinance = "^0.9.3" Pillow = "^8.2.0" pyportfolioopt = "^1.4.1" +statsmodels = "^0.12.2" [tool.poetry.dev-dependencies] pytest = "^6.2.2" From 5bedff3b36f501f86d7d7632d144a113275d2671 Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Sat, 17 Apr 2021 20:57:58 +0200 Subject: [PATCH 081/112] introduce start date to fetch the (latest) news from --- .../discovery/seeking_alpha_model.py | 16 ++++++++++------ .../discovery/seeking_alpha_view.py | 15 ++++++++++++++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index b573c506d3f..6041eb63f67 100755 --- a/gamestonk_terminal/discovery/seeking_alpha_model.py +++ b/gamestonk_terminal/discovery/seeking_alpha_model.py @@ -91,12 +91,14 @@ def get_articles_html(url_articles: str) -> str: return articles_html -def get_article_list(num: int) -> List[dict]: +def get_article_list(start_date: datetime, num: int) -> List[dict]: """Returns a list of latest articles Parameters ---------- - pages : int + start_date : datetime + Starting date + num : int Number of articles Returns @@ -122,13 +124,15 @@ def get_article_list(num: int) -> List[dict]: if not article_url.startswith("/news/"): continue article_id = article_url.split("/")[2].split("-")[0] - + article_date = datetime.strptime( + item_row["data-last-date"], "%Y-%m-%d %H:%M:%S %z" + ) + if start_date.date() < article_date.date(): + continue articles.append( { "title": item.text, - "publishedAt": datetime.strptime( - item_row["data-last-date"], "%Y-%m-%d %H:%M:%S %z" - ).strftime("%Y-%m-%d %H:%M:%S"), + "publishedAt": article_date.strftime("%Y-%m-%d %H:%M:%S"), "url": "https://seekingalpha.com" + article_url, "id": article_id, } diff --git a/gamestonk_terminal/discovery/seeking_alpha_view.py b/gamestonk_terminal/discovery/seeking_alpha_view.py index a11c548be72..7aedb3b8bd2 100644 --- a/gamestonk_terminal/discovery/seeking_alpha_view.py +++ b/gamestonk_terminal/discovery/seeking_alpha_view.py @@ -4,9 +4,11 @@ __docformat__ = "numpy" import argparse from typing import List import pandas as pd +from datetime import datetime from gamestonk_terminal.helper_funcs import ( check_positive, parse_known_args_and_warn, + valid_date, ) from gamestonk_terminal.discovery import seeking_alpha_model @@ -98,6 +100,15 @@ def latest_news_view(other_args: List[str]): default=10, help="number of latest articles being printed", ) + parser.add_argument( + "-d", + "--date", + action="store", + dest="n_date", + type=valid_date, + default=datetime.now().strftime("%Y-%m-%d"), + help="starting date of latest articles being printed", + ) if other_args: if "-" not in other_args[0]: @@ -109,7 +120,9 @@ def latest_news_view(other_args: List[str]): # User wants to see all latest news if ns_parser.n_id == -1: - articles = seeking_alpha_model.get_article_list(ns_parser.n_num) + articles = seeking_alpha_model.get_article_list( + ns_parser.n_date, ns_parser.n_num + ) for idx, article in enumerate(articles): print( article["publishedAt"].replace("T", " ").replace("Z", ""), From 2adde65ab4dbaa3401e7fae606c3e725dec21579 Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Sat, 17 Apr 2021 21:03:27 +0200 Subject: [PATCH 082/112] fix/shorten the help strings --- gamestonk_terminal/discovery/seeking_alpha_view.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gamestonk_terminal/discovery/seeking_alpha_view.py b/gamestonk_terminal/discovery/seeking_alpha_view.py index 7aedb3b8bd2..1670cc3e084 100644 --- a/gamestonk_terminal/discovery/seeking_alpha_view.py +++ b/gamestonk_terminal/discovery/seeking_alpha_view.py @@ -89,7 +89,7 @@ def latest_news_view(other_args: List[str]): dest="n_id", type=check_positive, default=-1, - help="article number found on Seeking Alpha website", + help="article ID number", ) parser.add_argument( "-n", @@ -98,7 +98,7 @@ def latest_news_view(other_args: List[str]): dest="n_num", type=check_positive, default=10, - help="number of latest articles being printed", + help="number of articles being printed", ) parser.add_argument( "-d", @@ -107,7 +107,7 @@ def latest_news_view(other_args: List[str]): dest="n_date", type=valid_date, default=datetime.now().strftime("%Y-%m-%d"), - help="starting date of latest articles being printed", + help="starting date", ) if other_args: @@ -172,7 +172,7 @@ def trending_news_view(other_args: List[str]): dest="n_id", type=check_positive, default=-1, - help="article number found on Seeking Alpha website", + help="article ID number", ) parser.add_argument( "-n", @@ -181,7 +181,7 @@ def trending_news_view(other_args: List[str]): dest="n_num", type=check_positive, default=10, - help="number of trending articles being printed", + help="number of articles being printed", ) if other_args: From d1b1df7f9e188ff9a6f1da6ae2bc7f5169906d54 Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Sat, 17 Apr 2021 21:35:18 +0200 Subject: [PATCH 083/112] update README.md --- gamestonk_terminal/discovery/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/gamestonk_terminal/discovery/README.md b/gamestonk_terminal/discovery/README.md index d892d25c1e7..6b20d1b25f1 100644 --- a/gamestonk_terminal/discovery/README.md +++ b/gamestonk_terminal/discovery/README.md @@ -229,12 +229,13 @@ Spectrum of sectors, industry, country. [Source: Finviz] ## latest ``` -usage: latest [-i N_ID] [-n N_NUM] +usage: latest [-i N_ID] [-n N_NUM] [-d DATE] ``` Latest news articles. [Source: Seeking Alpha] -* -i : article number found on Seeking Alpha website -* -n : number of latest articles being printed. Default 10. +* -i : Article ID number. +* -n : Number of articles being printed. Default 10. +* -d : Date of news article. latest @@ -246,8 +247,8 @@ usage: trending [-i N_ID] [-n N_NUM] ``` Trending news articles. [Source: Seeking Alpha] -* -i : article number found on Seeking Alpha website -* -n : number of trending articles being printed. Default 10. +* -i : Article ID number. +* -n : Number of articles being printed. Default 10. trending From 4dfa8ffe5be7d76af53519b05471e68913f8cdcb Mon Sep 17 00:00:00 2001 From: didier Date: Sat, 17 Apr 2021 22:35:38 +0100 Subject: [PATCH 084/112] fix tabulate --- poetry.lock | 4 ++-- pyproject.toml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 87eb1a904e2..4583f7782c7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2460,7 +2460,7 @@ name = "tabulate" version = "0.8.9" description = "Pretty-print tabular data" category = "main" -optional = true +optional = false python-versions = "*" [package.extras] @@ -2918,7 +2918,7 @@ prediction = ["fbprophet", "tensorflow", "pmdarima", "transformers", "flair", "t [metadata] lock-version = "1.1" python-versions = "^3.6.8" -content-hash = "b328b5fa451201792194e1919c729e3e0105b613ad2900405f65280f6afdb3aa" +content-hash = "623e5809320b1fb71c692bdd6cdcb9ea3419f3a5b3592e6c755816bd493769bc" [metadata.files] absl-py = [ diff --git a/pyproject.toml b/pyproject.toml index 17ae81736e7..3d1b8cd5967 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,7 @@ finvizfinance = "^0.9.3" Pillow = "^8.2.0" pyportfolioopt = "^1.4.1" statsmodels = "^0.12.2" +tabulate = "^0.8.9" [tool.poetry.dev-dependencies] pytest = "^6.2.2" From 638e1d55d4753a151aca856a0199348e6270af10 Mon Sep 17 00:00:00 2001 From: didier Date: Sat, 17 Apr 2021 22:40:37 +0100 Subject: [PATCH 085/112] make pmdarima not option --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 4583f7782c7..0a01734d46e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1708,7 +1708,7 @@ name = "pmdarima" version = "1.8.0" description = "Python's forecast::auto.arima equivalent" category = "main" -optional = true +optional = false python-versions = ">=3.6" [package.dependencies] From d0c6c4ba8451fb1f333119c80b6de443a4a65a85 Mon Sep 17 00:00:00 2001 From: didier Date: Sat, 17 Apr 2021 22:52:53 +0100 Subject: [PATCH 086/112] make pmdarima not optional --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3d1b8cd5967..12ccd7ebcc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ papermill = "^2.3.2" torch = {version = "1.7.1", optional = true} transformers = {version = "3.5.1", optional = true} fbprophet = {version = "0.6", optional = true} -pmdarima = {version = "1.8.0", optional = true} +pmdarima = {version = "1.8.0", optional = false} tensorflow = {version = "2.4.1", optional = true} flair = {version = "0.7", optional = true} holidays = "^0.10" From 57d290ccbb3fe78c4916ef31ee9e5efb71238c86 Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 18 Apr 2021 02:06:19 +0100 Subject: [PATCH 087/112] Add dark pools (ATS) vs Non-ATS data over time --- .../due_diligence/dd_controller.py | 7 + .../due_diligence/finra_view.py | 272 ++++++++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 gamestonk_terminal/due_diligence/finra_view.py diff --git a/gamestonk_terminal/due_diligence/dd_controller.py b/gamestonk_terminal/due_diligence/dd_controller.py index 14ecab8fc40..1324cf79cd1 100644 --- a/gamestonk_terminal/due_diligence/dd_controller.py +++ b/gamestonk_terminal/due_diligence/dd_controller.py @@ -13,6 +13,7 @@ from gamestonk_terminal.due_diligence import market_watch_view as mw_view from gamestonk_terminal.due_diligence import quandl_view as q_view from gamestonk_terminal.due_diligence import reddit_view as r_view from gamestonk_terminal.due_diligence import news_view +from gamestonk_terminal.due_diligence import finra_view from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import get_flair from gamestonk_terminal.menu import session @@ -37,6 +38,7 @@ class DueDiligenceController: "analyst", "warnings", "sec", + "dp", ] def __init__(self, stock: DataFrame, ticker: str, start: str, interval: str): @@ -97,6 +99,7 @@ class DueDiligenceController: print( " warnings company warnings according to Sean Seah book [Market Watch]" ) + print(" dp dark pools vs OTC data [FINRA]") print("") def switch(self, an_input: str): @@ -175,6 +178,10 @@ class DueDiligenceController: """ Process short command """ q_view.short_interest(other_args, self.ticker, self.start) + def call_dp(self, other_args: List[str]): + """ Process dp command """ + finra_view.dark_pool(other_args, self.ticker) + def menu(stock: DataFrame, ticker: str, start: str, interval: str): """Due Diligence Menu diff --git a/gamestonk_terminal/due_diligence/finra_view.py b/gamestonk_terminal/due_diligence/finra_view.py new file mode 100644 index 00000000000..13767897840 --- /dev/null +++ b/gamestonk_terminal/due_diligence/finra_view.py @@ -0,0 +1,272 @@ +""" FINRA View """ +__docformat__ = "numpy" + +import argparse +from typing import List +import requests +import pandas as pd +from matplotlib import pyplot as plt +import matplotlib.dates as mdates +from gamestonk_terminal.config_plot import PLOT_DPI +from gamestonk_terminal import feature_flags as gtff +from gamestonk_terminal.helper_funcs import parse_known_args_and_warn, plot_autoscale + + +def getFINRAweeks(tier, is_ats) -> List: + """Get FINRA weeks + + Parameters + ---------- + tier : str + Stock tier between T1, T2, or OTCE + is_ats : bool + ATS data if true, NON-ATS otherwise + + Returns + ---------- + List + List of response data + """ + req_hdr = {"Accept": "application/json", "Content-Type": "application/json"} + + req_data = { + "compareFilters": [ + { + "compareType": "EQUAL", + "fieldName": "summaryTypeCode", + "fieldValue": "ATS_W_SMBL" if is_ats else "OTC_W_SMBL", + }, + { + "compareType": "EQUAL", + "fieldName": "tierIdentifier", + "fieldValue": tier, + }, + ], + "delimiter": "|", + "fields": ["weekStartDate"], + "limit": 27, + "quoteValues": False, + "sortFields": ["-weekStartDate"], + } + + response = requests.post( + "https://api.finra.org/data/group/otcMarket/name/weeklyDownloadDetails", + headers=req_hdr, + json=req_data, + ) + + return response.json() if response.status_code == 200 else list() + + +def getFINRAdata(weekStartDate, tier, ticker, is_ats) -> List: + """Get FINRA data + + Parameters + ---------- + weekStartDate : str + Weekly data to get FINRA data + tier : str + Stock tier between T1, T2, or OTCE + ticker : str + Stock ticker to get data from + is_ats : bool + ATS data if true, NON-ATS otherwise + + Returns + ---------- + str + Status code from request + List + List of response data + """ + req_hdr = {"Accept": "application/json", "Content-Type": "application/json"} + + l_cmp_filters = [ + { + "compareType": "EQUAL", + "fieldName": "weekStartDate", + "fieldValue": weekStartDate, + }, + {"compareType": "EQUAL", "fieldName": "tierIdentifier", "fieldValue": tier}, + { + "compareType": "EQUAL", + "description": "", + "fieldName": "summaryTypeCode", + "fieldValue": "ATS_W_SMBL" if is_ats else "OTC_W_SMBL", + }, + ] + + if ticker: + l_cmp_filters.append( + { + "compareType": "EQUAL", + "fieldName": "issueSymbolIdentifier", + "fieldValue": ticker, + } + ) + + req_data = { + "compareFilters": l_cmp_filters, + "delimiter": "|", + "fields": [ + "issueSymbolIdentifier", + "totalWeeklyShareQuantity", + "totalWeeklyTradeCount", + "lastUpdateDate", + ], + "limit": 5000, + "quoteValues": False, + "sortFields": ["totalWeeklyShareQuantity"], + } + + response = requests.post( + "https://api.finra.org/data/group/otcMarket/name/weeklySummary", + headers=req_hdr, + json=req_data, + ) + + return ( + response.status_code, + response.json() if response.status_code == 200 else list(), + ) + + +def getTickerFINRAdata(ticker): + """Get all FINRA data associated with a ticker + + Parameters + ---------- + ticker : str + Stock ticker to get data from + + Returns + ---------- + pd.DataFrame + Dark Pools (ATS) Data + pd.DataFrame + OTC (Non-ATS) Data + """ + tiers = ["T1", "T2", "OTCE"] + + l_data = list() + for tier in tiers: + for d_week in getFINRAweeks(tier, is_ats=True): + status_code, response = getFINRAdata( + d_week["weekStartDate"], tier, ticker, True + ) + if status_code == 200: + if response: + d_data = response[0] + d_data.update(d_week) + l_data.append(d_data) + else: + break + + df_ats = pd.DataFrame(l_data) + df_ats = df_ats.sort_values("weekStartDate") + df_ats = df_ats.set_index("weekStartDate") + + l_data = list() + for tier in tiers: + for d_week in getFINRAweeks(tier, is_ats=False): + status_code, response = getFINRAdata( + d_week["weekStartDate"], tier, ticker, False + ) + if status_code == 200: + if response: + d_data = response[0] + d_data.update(d_week) + l_data.append(d_data) + else: + break + + df_otc = pd.DataFrame(l_data) + df_otc = df_otc.sort_values("weekStartDate") + df_otc = df_otc.set_index("weekStartDate") + + return df_ats, df_otc + + +def plot_dark_pools(ticker: str, ats: pd.DataFrame, otc: pd.DataFrame): + """Plots ATS and NON-ATS data + + Parameters + ---------- + ticker : str + Stock ticker to get data from + ats : pd.DataFrame + Dark Pools (ATS) Data + otc : pd.DataFrame + OTC (Non-ATS) Data + """ + _, axData = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI) + + plt.subplot(3, 1, (1, 2)) + plt.bar( + ats.index, + (ats["totalWeeklyShareQuantity"] + otc["totalWeeklyShareQuantity"]) / 1_000_000, + color="tab:orange", + ) + plt.bar(otc.index, otc["totalWeeklyShareQuantity"] / 1_000_000, color="tab:blue") + plt.ylabel("Total Weekly Shares [Million]") + plt.legend(["ATS", "OTC"]) + plt.grid(b=True, which="major", color="#666666", linestyle="-", alpha=0.2) + plt.title(f"Dark Pools (ATS) vs OTC (Non-ATS) Data for {ticker}") + + # _ = axData.twinx() + plt.subplot(313) + plt.plot( + ats.index, + ats["totalWeeklyShareQuantity"] / ats["totalWeeklyTradeCount"], + color="tab:orange", + ) + plt.plot( + otc.index, + otc["totalWeeklyShareQuantity"] / otc["totalWeeklyTradeCount"], + color="tab:blue", + ) + plt.ylabel("Shares per Trade") + plt.legend(["ATS", "OTC"]) + plt.grid(b=True, which="major", color="#666666", linestyle="-", alpha=0.2) + plt.gcf().autofmt_xdate() + plt.xlabel("Weeks") + + if gtff.USE_ION: + plt.ion() + + plt.show() + + +def dark_pool(other_args: List[str], ticker: str): + """Display barchart of dark pool (ATS) and OTC (Non ATS) data + + Parameters + ---------- + other_args : List[str] + argparse other args + ticker : str + Stock ticker + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="dp", + description="Display barchart of dark pool (ATS) and OTC (Non ATS) data [Source: FINRA]", + ) + + if 1: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + df_ats, df_otc = getTickerFINRAdata(ticker) + + plot_dark_pools(ticker, df_ats, df_otc) + + print("") + + """ + except Exception as e: + print(e) + print("") + return + """ From 0032e12abc99486538ab9e8c8cdb2852eb203bad Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sun, 18 Apr 2021 02:08:54 +0100 Subject: [PATCH 088/112] Update README.md --- gamestonk_terminal/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 192f21ca288..eb3dfae535b 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -213,6 +213,7 @@ Command|Description|Source `sec` |SEC filings |[MarketWatch](https://www.marketwatch.com/) `short` |short interest |[Quandl](https://www.quandl.com/) `warnings` |company warnings according to Sean Seah book |[MarketWatch](https://www.marketwatch.com/) +`dp` |dark pools (ATS) vs OTC data [FINRA](https://www.finra.org/#/)   From e04e4053c0ebdba9eff7e5065808d0874767bb6c Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 18 Apr 2021 02:09:58 +0100 Subject: [PATCH 089/112] remove unused import --- gamestonk_terminal/due_diligence/finra_view.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gamestonk_terminal/due_diligence/finra_view.py b/gamestonk_terminal/due_diligence/finra_view.py index 13767897840..a3358a71a5b 100644 --- a/gamestonk_terminal/due_diligence/finra_view.py +++ b/gamestonk_terminal/due_diligence/finra_view.py @@ -6,7 +6,6 @@ from typing import List import requests import pandas as pd from matplotlib import pyplot as plt -import matplotlib.dates as mdates from gamestonk_terminal.config_plot import PLOT_DPI from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import parse_known_args_and_warn, plot_autoscale From 477751915c448841bac6c1849f8153bcf0676b7f Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sun, 18 Apr 2021 02:12:19 +0100 Subject: [PATCH 090/112] Update README.md --- gamestonk_terminal/due_diligence/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/gamestonk_terminal/due_diligence/README.md b/gamestonk_terminal/due_diligence/README.md index 70bfb49340b..472867b0f7e 100644 --- a/gamestonk_terminal/due_diligence/README.md +++ b/gamestonk_terminal/due_diligence/README.md @@ -24,6 +24,8 @@ This menu aims to help in due-diligence of a pre-loaded stock, and the usage of * short interest [Quandl] * [warnings](#warnings) * company warnings according to Sean Seah book [Market Watch] +* [dp](#dp) + * dark pools (ATS) vs OTC data [FINRA] ## news @@ -157,3 +159,15 @@ Sean Seah warnings. Check: Consistent historical earnings per share; Consistentl * -d : print insights into warnings calculation. Default False. warnings + +## dp + +```text +usage: dp +``` + +Display barchart of dark pool (ATS) and OTC (Non ATS) data + +![dp](https://user-images.githubusercontent.com/25267873/115130908-7987b580-9feb-11eb-8bca-1999174178d0.png) + + From 73681c2d75eff6dfb6fc88599849914eb9d6184f Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 18 Apr 2021 02:35:30 +0100 Subject: [PATCH 091/112] Improve code of dd/dp --- .../due_diligence/dd_controller.py | 2 +- .../due_diligence/finra_view.py | 88 +++++++++++++------ 2 files changed, 63 insertions(+), 27 deletions(-) diff --git a/gamestonk_terminal/due_diligence/dd_controller.py b/gamestonk_terminal/due_diligence/dd_controller.py index 1324cf79cd1..378499f05cd 100644 --- a/gamestonk_terminal/due_diligence/dd_controller.py +++ b/gamestonk_terminal/due_diligence/dd_controller.py @@ -99,7 +99,7 @@ class DueDiligenceController: print( " warnings company warnings according to Sean Seah book [Market Watch]" ) - print(" dp dark pools vs OTC data [FINRA]") + print(" dp dark pools (ATS) vs OTC data [FINRA]") print("") def switch(self, an_input: str): diff --git a/gamestonk_terminal/due_diligence/finra_view.py b/gamestonk_terminal/due_diligence/finra_view.py index a3358a71a5b..ea3e80bfa71 100644 --- a/gamestonk_terminal/due_diligence/finra_view.py +++ b/gamestonk_terminal/due_diligence/finra_view.py @@ -2,7 +2,7 @@ __docformat__ = "numpy" import argparse -from typing import List +from typing import List, Any import requests import pandas as pd from matplotlib import pyplot as plt @@ -57,7 +57,7 @@ def getFINRAweeks(tier, is_ats) -> List: return response.json() if response.status_code == 200 else list() -def getFINRAdata(weekStartDate, tier, ticker, is_ats) -> List: +def getFINRAdata(weekStartDate, tier, ticker, is_ats) -> [int, List]: """Get FINRA data Parameters @@ -130,7 +130,7 @@ def getFINRAdata(weekStartDate, tier, ticker, is_ats) -> List: ) -def getTickerFINRAdata(ticker): +def getTickerFINRAdata(ticker) -> [pd.DataFrame, pd.DataFrame]: """Get all FINRA data associated with a ticker Parameters @@ -162,8 +162,9 @@ def getTickerFINRAdata(ticker): break df_ats = pd.DataFrame(l_data) - df_ats = df_ats.sort_values("weekStartDate") - df_ats = df_ats.set_index("weekStartDate") + if not df_ats.empty: + df_ats = df_ats.sort_values("weekStartDate") + df_ats = df_ats.set_index("weekStartDate") l_data = list() for tier in tiers: @@ -180,8 +181,9 @@ def getTickerFINRAdata(ticker): break df_otc = pd.DataFrame(l_data) - df_otc = df_otc.sort_values("weekStartDate") - df_otc = df_otc.set_index("weekStartDate") + if not df_otc.empty: + df_otc = df_otc.sort_values("weekStartDate") + df_otc = df_otc.set_index("weekStartDate") return df_ats, df_otc @@ -201,31 +203,62 @@ def plot_dark_pools(ticker: str, ats: pd.DataFrame, otc: pd.DataFrame): _, axData = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI) plt.subplot(3, 1, (1, 2)) - plt.bar( - ats.index, - (ats["totalWeeklyShareQuantity"] + otc["totalWeeklyShareQuantity"]) / 1_000_000, - color="tab:orange", - ) - plt.bar(otc.index, otc["totalWeeklyShareQuantity"] / 1_000_000, color="tab:blue") + if not ats.empty and not otc.empty: + plt.bar( + ats.index, + (ats["totalWeeklyShareQuantity"] + otc["totalWeeklyShareQuantity"]) + / 1_000_000, + color="tab:orange", + ) + plt.bar( + otc.index, otc["totalWeeklyShareQuantity"] / 1_000_000, color="tab:blue" + ) + plt.legend(["ATS", "OTC"]) + + elif not ats.empty: + plt.bar( + ats.index, + ats["totalWeeklyShareQuantity"] / 1_000_000, + color="tab:orange", + ) + plt.legend(["ATS"]) + + elif not otc.empty: + plt.bar( + otc.index, otc["totalWeeklyShareQuantity"] / 1_000_000, color="tab:blue" + ) + plt.legend(["OTC"]) + plt.ylabel("Total Weekly Shares [Million]") - plt.legend(["ATS", "OTC"]) plt.grid(b=True, which="major", color="#666666", linestyle="-", alpha=0.2) plt.title(f"Dark Pools (ATS) vs OTC (Non-ATS) Data for {ticker}") - # _ = axData.twinx() plt.subplot(313) - plt.plot( - ats.index, - ats["totalWeeklyShareQuantity"] / ats["totalWeeklyTradeCount"], - color="tab:orange", - ) - plt.plot( - otc.index, - otc["totalWeeklyShareQuantity"] / otc["totalWeeklyTradeCount"], - color="tab:blue", - ) + if not ats.empty: + plt.plot( + ats.index, + ats["totalWeeklyShareQuantity"] / ats["totalWeeklyTradeCount"], + color="tab:orange", + ) + plt.legend(["ATS"]) + + if not otc.empty: + plt.plot( + otc.index, + otc["totalWeeklyShareQuantity"] / otc["totalWeeklyTradeCount"], + color="tab:blue", + ) + plt.legend(["ATS", "OTC"]) + + else: + plt.plot( + otc.index, + otc["totalWeeklyShareQuantity"] / otc["totalWeeklyTradeCount"], + color="tab:blue", + ) + plt.legend(["OTC"]) + plt.ylabel("Shares per Trade") - plt.legend(["ATS", "OTC"]) plt.grid(b=True, which="major", color="#666666", linestyle="-", alpha=0.2) plt.gcf().autofmt_xdate() plt.xlabel("Weeks") @@ -259,6 +292,9 @@ def dark_pool(other_args: List[str], ticker: str): df_ats, df_otc = getTickerFINRAdata(ticker) + if df_ats.empty and df_otc.empty: + print("No ticker data found!") + plot_dark_pools(ticker, df_ats, df_otc) print("") From eae456cc3f9c1022957a77888c16964150c4e18d Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 18 Apr 2021 02:37:29 +0100 Subject: [PATCH 092/112] remove unused import --- gamestonk_terminal/due_diligence/finra_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gamestonk_terminal/due_diligence/finra_view.py b/gamestonk_terminal/due_diligence/finra_view.py index ea3e80bfa71..47ce2f7c817 100644 --- a/gamestonk_terminal/due_diligence/finra_view.py +++ b/gamestonk_terminal/due_diligence/finra_view.py @@ -2,7 +2,7 @@ __docformat__ = "numpy" import argparse -from typing import List, Any +from typing import List import requests import pandas as pd from matplotlib import pyplot as plt From e02a4a8baaa0c7cbfedb679e501a03512ca5b559 Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 18 Apr 2021 02:49:25 +0100 Subject: [PATCH 093/112] fix typing --- gamestonk_terminal/due_diligence/finra_view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gamestonk_terminal/due_diligence/finra_view.py b/gamestonk_terminal/due_diligence/finra_view.py index 47ce2f7c817..ab4fc2465aa 100644 --- a/gamestonk_terminal/due_diligence/finra_view.py +++ b/gamestonk_terminal/due_diligence/finra_view.py @@ -2,7 +2,7 @@ __docformat__ = "numpy" import argparse -from typing import List +from typing import List, Tuple import requests import pandas as pd from matplotlib import pyplot as plt @@ -57,7 +57,7 @@ def getFINRAweeks(tier, is_ats) -> List: return response.json() if response.status_code == 200 else list() -def getFINRAdata(weekStartDate, tier, ticker, is_ats) -> [int, List]: +def getFINRAdata(weekStartDate, tier, ticker, is_ats) -> Tuple[int, List]: """Get FINRA data Parameters @@ -130,7 +130,7 @@ def getFINRAdata(weekStartDate, tier, ticker, is_ats) -> [int, List]: ) -def getTickerFINRAdata(ticker) -> [pd.DataFrame, pd.DataFrame]: +def getTickerFINRAdata(ticker) -> Tuple[pd.DataFrame, pd.DataFrame]: """Get all FINRA data associated with a ticker Parameters From 33ced3512928597a9754d89b53a9f42fd6f09418 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sun, 18 Apr 2021 20:14:28 +0100 Subject: [PATCH 094/112] Update ROADMAP.md --- ROADMAP.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ROADMAP.md b/ROADMAP.md index 82d5b910ed9..d47c8d9c7d3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -79,6 +79,7 @@ --- ## Due Diligence +* [x] Add dark pools (ATS) vs Non-ATS data over time (@didier) - [PR #363](https://github.com/DidierRLopes/GamestonkTerminal/pull/363) **NEXT** From c66c74a26d051712bac4719fd35ae8d013e96a63 Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 18 Apr 2021 21:52:29 +0100 Subject: [PATCH 095/112] fix bug for yahoo finance tickers from other markets (e.g. SBIN.NS) --- terminal.py | 63 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/terminal.py b/terminal.py index b4c9e2754f9..3db2b090158 100644 --- a/terminal.py +++ b/terminal.py @@ -196,26 +196,51 @@ def main(): b_quit = mill.papermill_menu() elif ns_known_args.opt == "ba": - b_quit = ba_controller.menu(s_ticker, s_start) + b_quit = ba_controller.menu( + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, s_start + ) elif ns_known_args.opt == "res": - b_quit = rm.res_menu(s_ticker, s_start, s_interval) + b_quit = rm.res_menu( + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, + s_start, + s_interval, + ) elif ns_known_args.opt == "ca": b_quit = ca_controller.menu(df_stock, s_ticker, s_start, s_interval) elif ns_known_args.opt == "fa": - b_quit = fam.fa_menu(s_ticker, s_start, s_interval) + b_quit = fam.fa_menu( + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, + s_start, + s_interval, + ) elif ns_known_args.opt == "ta": - b_quit = ta_controller.menu(df_stock, s_ticker, s_start, s_interval) + b_quit = ta_controller.menu( + df_stock, + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, + s_start, + s_interval, + ) elif ns_known_args.opt == "dd": - b_quit = dd_controller.menu(df_stock, s_ticker, s_start, s_interval) + b_quit = dd_controller.menu( + df_stock, + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, + s_start, + s_interval, + ) elif ns_known_args.opt == "eda": if s_interval == "1440min": - b_quit = eda_controller.menu(df_stock, s_ticker, s_start, s_interval) + b_quit = eda_controller.menu( + df_stock, + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, + s_start, + s_interval, + ) else: df_stock = yf.download(s_ticker, start=s_start, progress=False) df_stock = df_stock.rename( @@ -231,7 +256,12 @@ def main(): df_stock.index.name = "date" s_interval = "1440min" - b_quit = eda_controller.menu(df_stock, s_ticker, s_start, s_interval) + b_quit = eda_controller.menu( + df_stock, + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, + s_start, + s_interval, + ) elif ns_known_args.opt == "op": b_quit = op_controller.menu( @@ -273,7 +303,12 @@ def main(): continue if s_interval == "1440min": - b_quit = pred_controller.menu(df_stock, s_ticker, s_start, s_interval) + b_quit = pred_controller.menu( + df_stock, + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, + s_start, + s_interval, + ) # If stock data is intradaily, we need to get data again as prediction # techniques work on daily adjusted data. By default we load data from # Alpha Vantage because the historical data loaded gives a larger @@ -291,7 +326,10 @@ def main(): df_stock_pred = df_stock_pred.sort_index(ascending=True) df_stock_pred = df_stock_pred[s_start:] b_quit = pred_controller.menu( - df_stock_pred, s_ticker, s_start, s_interval="1440min" + df_stock_pred, + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, + s_start, + s_interval="1440min", ) except Exception as e: print(e) @@ -299,7 +337,12 @@ def main(): return elif ns_known_args.opt == "ra": - b_quit = ra_controller.menu(df_stock, s_ticker, s_start, s_interval) + b_quit = ra_controller.menu( + df_stock, + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, + s_start, + s_interval, + ) elif ns_known_args.opt == "scr": b_quit = screener_controller.menu() From 304585cdb1f0c7e9f31e5f48cca5630c06e9acd6 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sun, 18 Apr 2021 22:01:13 +0100 Subject: [PATCH 096/112] add discord server to readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 84a6007fdd1..df5e46c0181 100644 --- a/README.md +++ b/README.md @@ -363,6 +363,9 @@ Distributed under the MIT License. See [LICENSE](https://github.com/DidierRLopes [Artem Veremy](https://www.linkedin.com/in/veremey/) - artem@veremey.net [James Maslek](https://www.linkedin.com/in/james-maslek-b6810186/) - jmaslek11@gmail.com + +Feel free to share porn loss, memes or any questions at https://discord.gg/FmKjHKMn + ## Acknowledgments * [VICE article](https://www.vice.com/en/article/qjp9vp/gamestonk-terminal-is-a-diy-meme-stock-version-of-bloomberg-terminal) From bfeaa2b9a0be7232fef8f35954af4fc0dacd3137 Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Sun, 18 Apr 2021 23:01:21 +0200 Subject: [PATCH 097/112] add ftd command to dd menu --- gamestonk_terminal/due_diligence/README.md | 14 +++ .../due_diligence/dd_controller.py | 7 ++ gamestonk_terminal/due_diligence/sec_view.py | 89 +++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 gamestonk_terminal/due_diligence/sec_view.py diff --git a/gamestonk_terminal/due_diligence/README.md b/gamestonk_terminal/due_diligence/README.md index 70bfb49340b..297d07428fa 100644 --- a/gamestonk_terminal/due_diligence/README.md +++ b/gamestonk_terminal/due_diligence/README.md @@ -24,6 +24,8 @@ This menu aims to help in due-diligence of a pre-loaded stock, and the usage of * short interest [Quandl] * [warnings](#warnings) * company warnings according to Sean Seah book [Market Watch] +* [ftd](#ftd) + * fails-to-deliver data [SEC] ## news @@ -157,3 +159,15 @@ Sean Seah warnings. Check: Consistent historical earnings per share; Consistentl * -d : print insights into warnings calculation. Default False. warnings + +## ftd + +```text +usage: ftd [-n] +``` + +The fails-to-deliver data collected by SEC. Fails to deliver on a given day are a cumulative number of all fails outstanding until that day, plus new fails that occur that day, less fails that settle that day. See . [Source: SEC] + +* -n : number of latest fails-to-deliver being printed. Default 10. + +ftd diff --git a/gamestonk_terminal/due_diligence/dd_controller.py b/gamestonk_terminal/due_diligence/dd_controller.py index 14ecab8fc40..808df2ef426 100644 --- a/gamestonk_terminal/due_diligence/dd_controller.py +++ b/gamestonk_terminal/due_diligence/dd_controller.py @@ -13,6 +13,7 @@ from gamestonk_terminal.due_diligence import market_watch_view as mw_view from gamestonk_terminal.due_diligence import quandl_view as q_view from gamestonk_terminal.due_diligence import reddit_view as r_view from gamestonk_terminal.due_diligence import news_view +from gamestonk_terminal.due_diligence import sec_view from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import get_flair from gamestonk_terminal.menu import session @@ -37,6 +38,7 @@ class DueDiligenceController: "analyst", "warnings", "sec", + "ftd", ] def __init__(self, stock: DataFrame, ticker: str, start: str, interval: str): @@ -97,6 +99,7 @@ class DueDiligenceController: print( " warnings company warnings according to Sean Seah book [Market Watch]" ) + print(" ftd fails-to-deliver data [SEC]") print("") def switch(self, an_input: str): @@ -175,6 +178,10 @@ class DueDiligenceController: """ Process short command """ q_view.short_interest(other_args, self.ticker, self.start) + def call_ftd(self, other_args: List[str]): + """ Process ftd command """ + sec_view.fails_to_deliver(other_args, self.ticker) + def menu(stock: DataFrame, ticker: str, start: str, interval: str): """Due Diligence Menu diff --git a/gamestonk_terminal/due_diligence/sec_view.py b/gamestonk_terminal/due_diligence/sec_view.py new file mode 100644 index 00000000000..35f19df8b1c --- /dev/null +++ b/gamestonk_terminal/due_diligence/sec_view.py @@ -0,0 +1,89 @@ +""" SEC View """ +__docformat__ = "numpy" + +import argparse +from typing import List +import requests +import pandas as pd +from bs4 import BeautifulSoup +from gamestonk_terminal.helper_funcs import ( + get_user_agent, + check_positive, + parse_known_args_and_warn, +) + + +def fails_to_deliver(other_args: List[str], ticker: str): + """Display fails-to-deliver data for a given ticker + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-n", "10"] + ticker : str + Stock ticker + """ + + parser = argparse.ArgumentParser( + add_help=False, + prog="ftd", + description="""Prints latest fails-to-deliver data. [Source: SEC]""", + ) + parser.add_argument( + "-n", + "--num", + action="store", + dest="n_num", + type=check_positive, + default=10, + help="number of latest fails-to-deliver being printed", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + url_ftds = "https://www.sec.gov/data/foiadocsfailsdatahtm" + text_soup_ftds = BeautifulSoup( + requests.get(url_ftds, headers={"User-Agent": get_user_agent()}).text, + "lxml", + ) + + table = text_soup_ftds.find("table", {"class": "list"}) + links = table.findAll("a") + link_idx = 0 + ftds_data = pd.DataFrame() + while len(ftds_data) < ns_parser.n_num: + if link_idx > len(links): + break + link = links[link_idx] + url = "https://www.sec.gov" + link["href"] + all_ftds = pd.read_csv( + url, + compression="zip", + sep="|", + engine="python", + skipfooter=2, + usecols=[0, 2, 3, 5], + dtype={"QUANTITY (FAILS)": "int"}, + ) + tmp_ftds = all_ftds[all_ftds["SYMBOL"] == ticker] + del tmp_ftds["PRICE"] + del tmp_ftds["SYMBOL"] + # merge the data from this archive + ftds_data = pd.concat([ftds_data, tmp_ftds], ignore_index=True) + link_idx += 1 + + # clip away extra rows + ftds_data = ftds_data.sort_values("SETTLEMENT DATE", ascending=False)[ + 0 : ns_parser.n_num + ] + print( + ftds_data.rename( + columns={"SETTLEMENT DATE": "Date", "QUANTITY (FAILS)": "Quantity"} + ).to_string(index=False) + ) + + except Exception as e: + print(e, "\n") From 76de01a024c108d0969fc52d9eb311f8ec89ca06 Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 18 Apr 2021 22:27:06 +0100 Subject: [PATCH 098/112] improve bar width of option calls/puts --- gamestonk_terminal/options/volume_view.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/options/volume_view.py b/gamestonk_terminal/options/volume_view.py index a4f3062a960..4d32126c682 100644 --- a/gamestonk_terminal/options/volume_view.py +++ b/gamestonk_terminal/options/volume_view.py @@ -351,8 +351,9 @@ def plot_calls_volume_open_interest( df_calls["strike"], df_calls["volume"] + df_calls["openInterest"], color="lightgreen", + width=0.45, ) - plt.bar(df_calls["strike"], df_calls["volume"], color="green") + plt.bar(df_calls["strike"], df_calls["volume"], color="green", width=0.45) plt.title(f"{ticker} calls volumes for {exp_date} ") plt.legend(["Stock Price", "Open Interest", "Volume"]) @@ -443,8 +444,9 @@ def plot_puts_volume_open_interest( df_puts["strike"], -df_puts["volume"] - df_puts["openInterest"], color="pink", + width=0.45, ) - plt.bar(df_puts["strike"], -df_puts["volume"], color="red") + plt.bar(df_puts["strike"], -df_puts["volume"], color="red", width=0.45) plt.title(f"{ticker} puts volumes for {exp_date} ") plt.legend(["Stock Price", "Open Interest", "Volume"]) From 8407cccedb72ce76d3dbb3766c0da6fe3373bc73 Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 18 Apr 2021 22:29:15 +0100 Subject: [PATCH 099/112] Add new line after option chains --- gamestonk_terminal/options/chains_view.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gamestonk_terminal/options/chains_view.py b/gamestonk_terminal/options/chains_view.py index c05552a909e..8bc63ef24a4 100644 --- a/gamestonk_terminal/options/chains_view.py +++ b/gamestonk_terminal/options/chains_view.py @@ -201,6 +201,7 @@ def display_chains(symbol: str, expiry: str, other_args: List[str]): floatfmt=".2f", ) ) + print("") except Exception as e: print(e) From e9b5c2262cd53079251df5ac3e127cb71d67ad5c Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Sun, 18 Apr 2021 23:46:49 +0200 Subject: [PATCH 100/112] update dd controller and README. --- gamestonk_terminal/due_diligence/README.md | 12 ++++++++++++ gamestonk_terminal/due_diligence/dd_controller.py | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/gamestonk_terminal/due_diligence/README.md b/gamestonk_terminal/due_diligence/README.md index 472867b0f7e..2d0c9fb2d0c 100644 --- a/gamestonk_terminal/due_diligence/README.md +++ b/gamestonk_terminal/due_diligence/README.md @@ -26,6 +26,8 @@ This menu aims to help in due-diligence of a pre-loaded stock, and the usage of * company warnings according to Sean Seah book [Market Watch] * [dp](#dp) * dark pools (ATS) vs OTC data [FINRA] +* [ftd](#ftd) + * fails-to-deliver data [SEC] ## news @@ -170,4 +172,14 @@ Display barchart of dark pool (ATS) and OTC (Non ATS) data ![dp](https://user-images.githubusercontent.com/25267873/115130908-7987b580-9feb-11eb-8bca-1999174178d0.png) +## ftd +```text +usage: ftd [-n] +``` + +The fails-to-deliver data collected by SEC. Fails to deliver on a given day are a cumulative number of all fails outstanding until that day, plus new fails that occur that day, less fails that settle that day. See . [Source: SEC] + +* -n : number of latest fails-to-deliver being printed. Default 10. + +ftd diff --git a/gamestonk_terminal/due_diligence/dd_controller.py b/gamestonk_terminal/due_diligence/dd_controller.py index 378499f05cd..2dbe1379927 100644 --- a/gamestonk_terminal/due_diligence/dd_controller.py +++ b/gamestonk_terminal/due_diligence/dd_controller.py @@ -14,6 +14,7 @@ from gamestonk_terminal.due_diligence import quandl_view as q_view from gamestonk_terminal.due_diligence import reddit_view as r_view from gamestonk_terminal.due_diligence import news_view from gamestonk_terminal.due_diligence import finra_view +from gamestonk_terminal.due_diligence import sec_view from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import get_flair from gamestonk_terminal.menu import session @@ -39,6 +40,7 @@ class DueDiligenceController: "warnings", "sec", "dp", + "ftd", ] def __init__(self, stock: DataFrame, ticker: str, start: str, interval: str): @@ -100,6 +102,7 @@ class DueDiligenceController: " warnings company warnings according to Sean Seah book [Market Watch]" ) print(" dp dark pools (ATS) vs OTC data [FINRA]") + print(" ftd fails-to-deliver data [SEC]") print("") def switch(self, an_input: str): @@ -182,6 +185,10 @@ class DueDiligenceController: """ Process dp command """ finra_view.dark_pool(other_args, self.ticker) + def call_ftd(self, other_args: List[str]): + """ Process ftd command """ + sec_view.fails_to_deliver(other_args, self.ticker) + def menu(stock: DataFrame, ticker: str, start: str, interval: str): """Due Diligence Menu From d348d867ae0c2b1ee002d070da6190abeeb187ab Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sun, 18 Apr 2021 22:54:09 +0100 Subject: [PATCH 101/112] Improve options data readme --- gamestonk_terminal/options/README.md | 93 ++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 27 deletions(-) diff --git a/gamestonk_terminal/options/README.md b/gamestonk_terminal/options/README.md index 980be7495c4..4742badb005 100644 --- a/gamestonk_terminal/options/README.md +++ b/gamestonk_terminal/options/README.md @@ -1,50 +1,89 @@ -# OPTIONS DATA +# OPTIONS -This menu aims to give insight on options. options can cause significant share price movement, and option pricing gives -info about market sentiment for a ticker. +This menu aims to give insight on options. -* [volume](#volume) - * Show traded options for a given expiry [Yahoo Finance] +This menu aims to give insight on options. Options can cause significant share price movement, and option pricing gives info about market sentiment for a ticker. The usage of the following commands along with an example will be exploited below: +* [exp](#exp) + * See/set expiry date [Yahoo Finance] +* [voi](#voi) + * Volume + open interest options trading plot [Yahoo Finance] +* [vcalls](#vcalls) + * Calls volume + open interest plot [Yahoo Finance] +* [vputs](#vputs) + * Puts volume + open interest plot [Yahoo Finance] * [chains](#chains) - * Display option chains for + * Display option chains [Source: Tradier] -## volume + +## exp ```text -usage: volume -e yyyy-mm-dd +usage: exp [-d {0,1,2,3,4,5,6,7,8,9,10,11}] ``` -or +See/set expiry dates. [Source: Yahoo Finance] + +* -d : Expiry date index to set. + +exp + + +## voi ```text -usage: volume --expiry yyyy-mm-dd +usage: voi [-v MIN_VOL] [-m MIN_SP] [-M MAX_SP] ``` +Plots Volume + Open Interest of calls vs puts. [Source: Yahoo Finance] -Display volume graph for a date. [Source: Yahoo Finance] +* -v : Minimum volume (considering open interest) threshold of the plot. +* -m : Minimum strike price to consider in the plot. +* -M : Maximum strike price to consider in the plot. + +![voinio](https://user-images.githubusercontent.com/25267873/115161878-f7a19600-a097-11eb-889b-e9cc945f174d.png) + + +## vcalls + +```text +usage: vcalls [-m MIN_SP] [-M MAX_SP] +``` + +Plots Calls Volume + Open Interest. [Source: Yahoo Finance] + +* -m : Minimum strike price to consider in the plot. +* -M : Maximum strike price to consider in the plot. + +![vcalls](https://user-images.githubusercontent.com/25267873/115161874-f5d7d280-a097-11eb-93e2-3a2b20292f09.png) + + +## vputs + +```text +usage: vputs [-m MIN_SP] [-M MAX_SP] +``` + +Plots Puts Volume + Open Interest. [Source: Yahoo Finance] + +* -m : Minimum strike price to consider in the plot. +* -M : Maximum strike price to consider in the plot. + +![vputs](https://user-images.githubusercontent.com/25267873/115161873-f40e0f00-a097-11eb-8334-3d4f14b56766.png) -* -e, --expiry : expiration date. ## chains -Display a table of options chains. - -This data requires a Tradier sandbox developer account, which can be made [here](#https://tradier.com/products/market-data-api) ```` -usage: chains [--calls] [--puts] [-m MIN] [-M MAX] +usage: chains [--calls] [--puts] [-m MIN_SP] [-M MAX_SP] ```` -* --calls : Flag to only display call options -* --puts : Flag to display only put options -* -m : Minimum strike price to show -* -M : Maximum strike price to show -For combines tables, call options are on the left and put options are on the right of the strike +Display options chains. [Source: Tradier] -As an example, if I load my ticker and select an expiry, to view the options chain for strikes between $129 and $135, one would use: +* --calls : Flag to show calls only +* --puts : Flag to show puts only +* -m : Minimum strike price to consider. +* -M : Maximum strike price to consider. -```` -chains -m 129 -M 135 -```` -Which outputs: -![im](https://user-images.githubusercontent.com/18151143/115068041-b191da00-9ebf-11eb-941e-4e255787ab94.png) + +chains From b2932efee156223c534daec103f2ac74d10bb3b2 Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 18 Apr 2021 23:00:59 +0100 Subject: [PATCH 102/112] minor improvements --- gamestonk_terminal/options/chains_view.py | 4 +++- gamestonk_terminal/options/op_controller.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/options/chains_view.py b/gamestonk_terminal/options/chains_view.py index 8bc63ef24a4..d1a99a9d1e3 100644 --- a/gamestonk_terminal/options/chains_view.py +++ b/gamestonk_terminal/options/chains_view.py @@ -88,7 +88,9 @@ def get_option_chains(symbol: str, expiry: str) -> pd.DataFrame: def display_chains(symbol: str, expiry: str, other_args: List[str]): parser = argparse.ArgumentParser( - prog="chains", add_help=False, description="Display options chains" + prog="chains", + add_help=False, + description="Display option chains [Source: Tradier]", ) parser.add_argument( diff --git a/gamestonk_terminal/options/op_controller.py b/gamestonk_terminal/options/op_controller.py index 4caa51dbd63..4a37d485327 100644 --- a/gamestonk_terminal/options/op_controller.py +++ b/gamestonk_terminal/options/op_controller.py @@ -38,7 +38,7 @@ class OptionsController: parser = argparse.ArgumentParser( add_help=False, prog="exp", - description="""See expiry dates.""", + description="""See/set expiry dates. [Source: Yahoo Finance]""", ) parser.add_argument( "-d", From 84c80be012f2756828599b77a4eb65e892acf75e Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Mon, 19 Apr 2021 00:12:33 +0200 Subject: [PATCH 103/112] fix date formatting --- gamestonk_terminal/due_diligence/sec_view.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gamestonk_terminal/due_diligence/sec_view.py b/gamestonk_terminal/due_diligence/sec_view.py index 35f19df8b1c..01a512b552d 100644 --- a/gamestonk_terminal/due_diligence/sec_view.py +++ b/gamestonk_terminal/due_diligence/sec_view.py @@ -3,6 +3,7 @@ __docformat__ = "numpy" import argparse from typing import List +from datetime import datetime import requests import pandas as pd from bs4 import BeautifulSoup @@ -79,6 +80,9 @@ def fails_to_deliver(other_args: List[str], ticker: str): ftds_data = ftds_data.sort_values("SETTLEMENT DATE", ascending=False)[ 0 : ns_parser.n_num ] + ftds_data["SETTLEMENT DATE"] = ftds_data["SETTLEMENT DATE"].apply( + lambda x: datetime.strptime(str(x), "%Y%m%d").strftime("%Y-%m-%d") + ) print( ftds_data.rename( columns={"SETTLEMENT DATE": "Date", "QUANTITY (FAILS)": "Quantity"} From 58320dc09dd954fec9a7cbb394ae879c8d2a1576 Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Mon, 19 Apr 2021 00:36:20 +0200 Subject: [PATCH 104/112] update ROADMAP --- ROADMAP.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index d47c8d9c7d3..277662bd9a8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -80,6 +80,7 @@ ## Due Diligence * [x] Add dark pools (ATS) vs Non-ATS data over time (@didier) - [PR #363](https://github.com/DidierRLopes/GamestonkTerminal/pull/363) +* [x] Add failure to deliver command (@hinxx) - [PR #366](#https://github.com/DidierRLopes/GamestonkTerminal/pull/366) **NEXT** @@ -148,7 +149,7 @@ * [ ] Refactoring * [ ] Summaries / tear sheets (@jmaslek) -___ +___ ## Portfolio Optimization * [x] Basic Optimization through PyPortFolioOpt(@jmaslek) - [PR #329](https://github.com/DidierRLopes/GamestonkTerminal/pull/329) From 48d48cae1c9175d803aed651be5040eccccda975 Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Mon, 19 Apr 2021 01:01:14 +0200 Subject: [PATCH 105/112] plot bars instead of printing to console --- gamestonk_terminal/due_diligence/sec_view.py | 24 ++++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/gamestonk_terminal/due_diligence/sec_view.py b/gamestonk_terminal/due_diligence/sec_view.py index 01a512b552d..7dc0ee7ae4e 100644 --- a/gamestonk_terminal/due_diligence/sec_view.py +++ b/gamestonk_terminal/due_diligence/sec_view.py @@ -7,6 +7,9 @@ from datetime import datetime import requests import pandas as pd from bs4 import BeautifulSoup +from matplotlib import pyplot as plt +from gamestonk_terminal.config_plot import PLOT_DPI +from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import ( get_user_agent, check_positive, @@ -77,17 +80,24 @@ def fails_to_deliver(other_args: List[str], ticker: str): link_idx += 1 # clip away extra rows - ftds_data = ftds_data.sort_values("SETTLEMENT DATE", ascending=False)[ - 0 : ns_parser.n_num - ] + ftds_data = ftds_data.sort_values("SETTLEMENT DATE")[-ns_parser.n_num:] ftds_data["SETTLEMENT DATE"] = ftds_data["SETTLEMENT DATE"].apply( lambda x: datetime.strptime(str(x), "%Y%m%d").strftime("%Y-%m-%d") ) - print( - ftds_data.rename( - columns={"SETTLEMENT DATE": "Date", "QUANTITY (FAILS)": "Quantity"} - ).to_string(index=False) + + plt.bar( + ftds_data["SETTLEMENT DATE"], + ftds_data["QUANTITY (FAILS)"], ) + plt.ylabel("Number of shares") + plt.title(f"Fails-to-deliver Data for {ticker}") + plt.gcf().autofmt_xdate() + plt.xlabel("Days") + + if gtff.USE_ION: + plt.ion() + + plt.show() except Exception as e: print(e, "\n") From 79c3d0a40f71c3bb2e6b431bcc8844d44f0a007a Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Mon, 19 Apr 2021 01:03:51 +0200 Subject: [PATCH 106/112] fix checks --- gamestonk_terminal/due_diligence/sec_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gamestonk_terminal/due_diligence/sec_view.py b/gamestonk_terminal/due_diligence/sec_view.py index 7dc0ee7ae4e..94dabc09a57 100644 --- a/gamestonk_terminal/due_diligence/sec_view.py +++ b/gamestonk_terminal/due_diligence/sec_view.py @@ -80,7 +80,7 @@ def fails_to_deliver(other_args: List[str], ticker: str): link_idx += 1 # clip away extra rows - ftds_data = ftds_data.sort_values("SETTLEMENT DATE")[-ns_parser.n_num:] + ftds_data = ftds_data.sort_values("SETTLEMENT DATE")[-ns_parser.n_num :] ftds_data["SETTLEMENT DATE"] = ftds_data["SETTLEMENT DATE"].apply( lambda x: datetime.strptime(str(x), "%Y%m%d").strftime("%Y-%m-%d") ) From e0e9745cdd7199982179fc94315659f5445424d7 Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Mon, 19 Apr 2021 01:37:11 +0200 Subject: [PATCH 107/112] fix plot output --- gamestonk_terminal/due_diligence/sec_view.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/gamestonk_terminal/due_diligence/sec_view.py b/gamestonk_terminal/due_diligence/sec_view.py index 94dabc09a57..803cf8ecf3c 100644 --- a/gamestonk_terminal/due_diligence/sec_view.py +++ b/gamestonk_terminal/due_diligence/sec_view.py @@ -8,7 +8,7 @@ import requests import pandas as pd from bs4 import BeautifulSoup from matplotlib import pyplot as plt -from gamestonk_terminal.config_plot import PLOT_DPI +import matplotlib.dates as mdates from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import ( get_user_agent, @@ -82,15 +82,18 @@ def fails_to_deliver(other_args: List[str], ticker: str): # clip away extra rows ftds_data = ftds_data.sort_values("SETTLEMENT DATE")[-ns_parser.n_num :] ftds_data["SETTLEMENT DATE"] = ftds_data["SETTLEMENT DATE"].apply( - lambda x: datetime.strptime(str(x), "%Y%m%d").strftime("%Y-%m-%d") + lambda x: datetime.strptime(str(x), "%Y%m%d") ) plt.bar( ftds_data["SETTLEMENT DATE"], - ftds_data["QUANTITY (FAILS)"], + ftds_data["QUANTITY (FAILS)"] / 1000, ) - plt.ylabel("Number of shares") + plt.ylabel("Shares [K]") plt.title(f"Fails-to-deliver Data for {ticker}") + plt.grid(b=True, which="major", color="#666666", linestyle="-", alpha=0.2) + plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%Y/%m/%d")) + plt.gca().xaxis.set_major_locator(mdates.DayLocator(interval=7)) plt.gcf().autofmt_xdate() plt.xlabel("Days") From ca7a4da4302f92501fdd6d6fb87c7b697aa19e36 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Mon, 19 Apr 2021 08:44:44 +0100 Subject: [PATCH 108/112] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df5e46c0181..7724ad3300c 100644 --- a/README.md +++ b/README.md @@ -364,7 +364,7 @@ Distributed under the MIT License. See [LICENSE](https://github.com/DidierRLopes [James Maslek](https://www.linkedin.com/in/james-maslek-b6810186/) - jmaslek11@gmail.com -Feel free to share porn loss, memes or any questions at https://discord.gg/FmKjHKMn +Feel free to share loss porn, memes or any questions at https://discord.gg/FmKjHKMn ## Acknowledgments From 4ab29f483bba47fe00980f7e3c08d74c86c83705 Mon Sep 17 00:00:00 2001 From: didier Date: Mon, 19 Apr 2021 08:51:44 +0100 Subject: [PATCH 109/112] minor fixes --- gamestonk_terminal/README.md | 1 + gamestonk_terminal/due_diligence/sec_view.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index eb3dfae535b..fdaa9118d84 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -214,6 +214,7 @@ Command|Description|Source `short` |short interest |[Quandl](https://www.quandl.com/) `warnings` |company warnings according to Sean Seah book |[MarketWatch](https://www.marketwatch.com/) `dp` |dark pools (ATS) vs OTC data [FINRA](https://www.finra.org/#/) +`ftd` |display fails-to-deliver data [SEC](https://www.sec.gov)   diff --git a/gamestonk_terminal/due_diligence/sec_view.py b/gamestonk_terminal/due_diligence/sec_view.py index 803cf8ecf3c..9a8ecba46ed 100644 --- a/gamestonk_terminal/due_diligence/sec_view.py +++ b/gamestonk_terminal/due_diligence/sec_view.py @@ -102,5 +102,7 @@ def fails_to_deliver(other_args: List[str], ticker: str): plt.show() + print("") + except Exception as e: print(e, "\n") From ec0cab1b0dd263d7ed4ac94934dc259643ab1b7c Mon Sep 17 00:00:00 2001 From: didier Date: Mon, 19 Apr 2021 08:54:58 +0100 Subject: [PATCH 110/112] add a bit more dates to ftd --- gamestonk_terminal/due_diligence/sec_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gamestonk_terminal/due_diligence/sec_view.py b/gamestonk_terminal/due_diligence/sec_view.py index 9a8ecba46ed..a2528f87387 100644 --- a/gamestonk_terminal/due_diligence/sec_view.py +++ b/gamestonk_terminal/due_diligence/sec_view.py @@ -93,7 +93,7 @@ def fails_to_deliver(other_args: List[str], ticker: str): plt.title(f"Fails-to-deliver Data for {ticker}") plt.grid(b=True, which="major", color="#666666", linestyle="-", alpha=0.2) plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%Y/%m/%d")) - plt.gca().xaxis.set_major_locator(mdates.DayLocator(interval=7)) + plt.gca().xaxis.set_major_locator(mdates.DayLocator(interval=3)) plt.gcf().autofmt_xdate() plt.xlabel("Days") From b74d70a3d30cd68caa342596627ad7d85c56250c Mon Sep 17 00:00:00 2001 From: didier Date: Mon, 19 Apr 2021 08:58:13 +0100 Subject: [PATCH 111/112] update default shown to 1 month --- gamestonk_terminal/due_diligence/sec_view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/due_diligence/sec_view.py b/gamestonk_terminal/due_diligence/sec_view.py index a2528f87387..9eb3f02e8f8 100644 --- a/gamestonk_terminal/due_diligence/sec_view.py +++ b/gamestonk_terminal/due_diligence/sec_view.py @@ -39,7 +39,7 @@ def fails_to_deliver(other_args: List[str], ticker: str): action="store", dest="n_num", type=check_positive, - default=10, + default=20, help="number of latest fails-to-deliver being printed", ) @@ -93,7 +93,7 @@ def fails_to_deliver(other_args: List[str], ticker: str): plt.title(f"Fails-to-deliver Data for {ticker}") plt.grid(b=True, which="major", color="#666666", linestyle="-", alpha=0.2) plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%Y/%m/%d")) - plt.gca().xaxis.set_major_locator(mdates.DayLocator(interval=3)) + plt.gca().xaxis.set_major_locator(mdates.DayLocator(interval=7)) plt.gcf().autofmt_xdate() plt.xlabel("Days") From 5fe3746546223a92a5f34549361e977da09981a2 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Mon, 19 Apr 2021 09:00:03 +0100 Subject: [PATCH 112/112] Update README.md --- gamestonk_terminal/due_diligence/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gamestonk_terminal/due_diligence/README.md b/gamestonk_terminal/due_diligence/README.md index 2d0c9fb2d0c..cb8854b78e5 100644 --- a/gamestonk_terminal/due_diligence/README.md +++ b/gamestonk_terminal/due_diligence/README.md @@ -175,11 +175,12 @@ Display barchart of dark pool (ATS) and OTC (Non ATS) data ## ftd ```text -usage: ftd [-n] +usage: ftd [-n N_NUM] ``` The fails-to-deliver data collected by SEC. Fails to deliver on a given day are a cumulative number of all fails outstanding until that day, plus new fails that occur that day, less fails that settle that day. See . [Source: SEC] -* -n : number of latest fails-to-deliver being printed. Default 10. +* -n : number of latest fails-to-deliver being printed. Default 20. + +![ftd](https://user-images.githubusercontent.com/25267873/115201750-93a9bc80-a0ed-11eb-8f13-68508749259e.png) -ftd