diff --git a/README.md b/README.md index eb6c28d634a..54c5089fe7f 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,9 @@ @@ -120,6 +123,14 @@ Navigate into the folder with: `cd GamestonkTerminal/` conda install poetry ``` +5.5. If installing python 3.8 +``` +conda deactivate +conda activate gst +``` +*The `conda deactivate` -> `conda activate` in the middle is on purpose, this is sometimes required to avoid issues with poetry* + + 6. Install poetry dependencies ``` poetry install @@ -132,21 +143,6 @@ This is a library for package management, and ensures a smoother experience than python terminal.py ``` -### Advanced User Install - Python 3.8 - -*Note that the `conda deactivate` -> `conda activate` in the middle is on purpose, this is sometimes required to avoid issues with poetry* - -``` -conda create -n gst python=3.8.8 -conda activate gst -conda install poetry -conda deactivate -conda activate gst -poetry install -conda install -c conda-forge fbprophet numpy=1.19.5 hdf5=1.10.5 -poetry install -E prediction -``` - ### Advanced User Install - Machine Learning If you are an advanced user and use other Python distributions, we have several requirements.txt documents that you can pick from to download project dependencies. @@ -185,7 +181,7 @@ Note: The problem with docker is that it won't output matplotlib figures. * `pip install pystan --upgrade` * `poetry update --lock` -### Other issues +### Other Issues If you run into trouble with poetry and the advice above did not help, your best bet is to try @@ -215,6 +211,7 @@ If you run into trouble with poetry and the advice above did not help, your best 7. Submit a ticket on github ### ModuleNotFoundError + In the case when you run into an error of the form `ModuleNotFoundError: No module named '_______'`. The solution is to install the missing package via pip. @@ -223,6 +220,30 @@ If you get the error that `statsmodels` is not found, you would run Then please submit an issue so that we can address why that was not imported. +Please note that the package `pmdarima` needs to installed through `pip install` and not through `conda install`. + +### Update Terminal + +The terminal is constantly being updated with new features and bug fixes, hence, for your terminal to be update, you can run: +``` +git pull +``` +to get the latest changes. + +If this fails due to the fact that you had modified some python files, and there's a conflict with the updates, you can use: +``` +git stash +``` + +Then, re-run `poetry install` or `pip install -r requirements.txt` to get any new dependencies. + +Once installation is finished, you're ready to gamestonk. + +If you `stashed` your changes previously, you can un-stash them with: +``` +git stash pop +``` + ### API Keys The project is build around several different API calls, whether it is to access historical data or financials. 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 46e946a0161..2b6d79c7b2e 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -23,9 +23,11 @@ The main menu allows the following commands: ``` load -t S_TICKER [-s S_START_DATE] [-i {1,5,15,30,60}] ``` - * Load stock ticker to perform analysis on + * Load stock ticker to perform analysis on. When the data source is 'yf', an Indian ticker can be loaded by using '.NS' at the end, e.g. 'SBIN.NS'. See available market in https://help.yahoo.com/kb/exchanges-data-providers-yahoo-finance-sln2310.html. * -s : The starting date (format YYYY-MM-DD) of the stock * -i : Intraday stock minutes + * --source : Source of historical data. 'yf' and 'av' available. Default 'yf' + * -p : Pre/After market hours. Only works for 'yf' source, and intraday data **Note:** Until a ticker is loaded, the menu will only show *disc* and *sen* menu, as the others require a ticker being provided. @@ -33,6 +35,7 @@ load -t S_TICKER [-s S_START_DATE] [-i {1,5,15,30,60}] clear ``` * Clear previously loaded stock ticker. + ``` view -t S_TICKER [-s S_START_DATE] [-i {1,5,15,30,60}] [--type N_TYPE] ``` @@ -238,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/behavioural_analysis/google_view.py b/gamestonk_terminal/behavioural_analysis/google_view.py index 9b85d7a2dc1..9047f75e59c 100644 --- a/gamestonk_terminal/behavioural_analysis/google_view.py +++ b/gamestonk_terminal/behavioural_analysis/google_view.py @@ -99,7 +99,7 @@ def regions(l_args, s_ticker): plt.bar(df_interest_region.index, df_interest_region[s_ticker], width=0.8) plt.grid(b=True, which="major", color="#666666", linestyle="-") plt.ylabel("Interest [%]") - plt.xlabel("Time") + plt.xlabel("Region") plt.show() print("") diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py index 67b91f8b4da..f999ed092af 100644 --- a/gamestonk_terminal/comparison_analysis/ca_controller.py +++ b/gamestonk_terminal/comparison_analysis/ca_controller.py @@ -3,6 +3,7 @@ __docformat__ = "numpy" import argparse import requests +import random from typing import List from datetime import datetime import pandas as pd @@ -15,6 +16,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 from gamestonk_terminal.menu import session from prompt_toolkit.completion import NestedCompleter @@ -42,6 +44,7 @@ class ComparisonAnalysisController: "ownership", "performance", "technical", + "po", ] def __init__( @@ -107,6 +110,8 @@ class ComparisonAnalysisController: print(" performance brief performance comparison") print(" technical brief technical comparison") print("") + print(" > po portfolio optimization for selected tickers") + print("") return @staticmethod @@ -172,9 +177,11 @@ class ComparisonAnalysisController: if len(self.similar) > 10: print( - "\nThe limit of stocks to compare with are 10. Hence, the similar stocks list will be:" + "\nThe limit of stocks to compare with are 10. Hence, 10 random similar stocks will be displayed.", + "\nThe selected list will be:", ) - self.similar = self.similar[:10] + random.shuffle(self.similar) + self.similar = sorted(self.similar[:10]) print(", ".join(self.similar)) except Exception as e: @@ -312,6 +319,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""" + return po_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/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index c7a4ac40e33..07bb33a3302 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -4,6 +4,7 @@ __docformat__ = "numpy" import argparse import os from typing import List +from matplotlib import pyplot as plt from prompt_toolkit.completion import NestedCompleter from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import get_flair @@ -209,6 +210,8 @@ def menu(): an_input = input(f"{get_flair()} (disc)> ") try: + plt.close("all") + process_input = disc_controller.switch(an_input) if process_input is not None: diff --git a/gamestonk_terminal/due_diligence/business_insider_api.py b/gamestonk_terminal/due_diligence/business_insider_view.py similarity index 80% rename from gamestonk_terminal/due_diligence/business_insider_api.py rename to gamestonk_terminal/due_diligence/business_insider_view.py index 63ca11b3dc9..32a1995198c 100644 --- a/gamestonk_terminal/due_diligence/business_insider_api.py +++ b/gamestonk_terminal/due_diligence/business_insider_view.py @@ -1,6 +1,11 @@ +""" Business Insider View """ +__docformat__ = "numpy" + import argparse +from typing import List import json import re +from pandas.core.frame import DataFrame import requests import matplotlib.pyplot as plt import pandas as pd @@ -12,11 +17,30 @@ from gamestonk_terminal.helper_funcs import ( get_user_agent, parse_known_args_and_warn, ) +from gamestonk_terminal import feature_flags as gtff register_matplotlib_converters() -def price_target_from_analysts(l_args, df_stock, s_ticker, s_start, s_interval): +def price_target_from_analysts( + other_args: List[str], stock: DataFrame, ticker: str, start: str, interval: str +): + """Display analysts' price targets for a given stock + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-n", "10"] + stock : DataFrame + Due diligence stock dataframe + ticker : str + Due diligence ticker symbol + start : str + Start date of the stock data + interval : str + Stock data interval + """ + parser = argparse.ArgumentParser( add_help=False, prog="pt", @@ -34,12 +58,12 @@ def price_target_from_analysts(l_args, df_stock, s_ticker, s_start, s_interval): ) try: - ns_parser = parse_known_args_and_warn(parser, l_args) + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return url_market_business_insider = ( - f"https://markets.businessinsider.com/stocks/{s_ticker.lower()}-stock" + f"https://markets.businessinsider.com/stocks/{ticker.lower()}-stock" ) text_soup_market_business_insider = BeautifulSoup( requests.get( @@ -59,8 +83,7 @@ def price_target_from_analysts(l_args, df_stock, s_ticker, s_start, s_interval): d_analyst_data = json.loads(s_analyst_data) break - # pprint.pprint(d_analyst_data) - df_analyst_data = pd.DataFrame.from_dict(d_analyst_data["Markers"]) + df_analyst_data = pd.DataFrame.from_dict(d_analyst_data["Markers"]) # type: ignore df_analyst_data = df_analyst_data[ ["DateLabel", "Company", "InternalRating", "PriceTarget"] ] @@ -73,17 +96,17 @@ def price_target_from_analysts(l_args, df_stock, s_ticker, s_start, s_interval): df_analyst_data = df_analyst_data.set_index("Date") # Slice start of ratings - if s_start: - df_analyst_data = df_analyst_data[s_start:] + if start: + df_analyst_data = df_analyst_data[start:] # type: ignore - if s_interval == "1440min": - plt.plot(df_stock.index, df_stock["5. adjusted close"].values, lw=3) + if interval == "1440min": + plt.plot(stock.index, stock["5. adjusted close"].values, lw=3) # Intraday else: - plt.plot(df_stock.index, df_stock["4. close"].values, lw=3) + plt.plot(stock.index, stock["4. close"].values, lw=3) - if s_start: - plt.plot(df_analyst_data.groupby(by=["Date"]).mean()[s_start:]) + if start: + plt.plot(df_analyst_data.groupby(by=["Date"]).mean()[start:]) # type: ignore else: plt.plot(df_analyst_data.groupby(by=["Date"]).mean()) @@ -91,13 +114,17 @@ def price_target_from_analysts(l_args, df_stock, s_ticker, s_start, s_interval): plt.legend(["Closing Price", "Average Price Target", "Price Target"]) - plt.title(f"{s_ticker} (Time Series) and Price Target") - plt.xlim(df_stock.index[0], df_stock.index[-1]) + plt.title(f"{ticker} (Time Series) and Price Target") + plt.xlim(stock.index[0], stock.index[-1]) plt.xlabel("Time") plt.ylabel("Share Price ($)") 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("") @@ -115,7 +142,17 @@ def price_target_from_analysts(l_args, df_stock, s_ticker, s_start, s_interval): return -def estimates(l_args, s_ticker): +def estimates(other_args: List[str], ticker: str): + """Display analysts' estimates for a given ticker + + Parameters + ---------- + other_args : List[str] + argparse other args + ticker : str + Due diligence ticker symbol + """ + parser = argparse.ArgumentParser( add_help=False, prog="est", @@ -123,12 +160,12 @@ def estimates(l_args, s_ticker): ) try: - ns_parser = parse_known_args_and_warn(parser, l_args) + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return url_market_business_insider = ( - f"https://markets.businessinsider.com/stocks/{s_ticker.lower()}-stock" + f"https://markets.businessinsider.com/stocks/{ticker.lower()}-stock" ) text_soup_market_business_insider = BeautifulSoup( requests.get( @@ -274,7 +311,24 @@ def estimates(l_args, s_ticker): return -def insider_activity(l_args, df_stock, s_ticker, s_start, s_interval): +def insider_activity( + other_args: List[str], stock: DataFrame, ticker: str, start: str, interval: str +): + """Display insider activity + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-n", "10"] + stock : DataFrame + Due diligence stock dataframe + ticker : str + Due diligence ticker symbol + start : str + Start date of the stock data + interval : str + Stock data interval + """ parser = argparse.ArgumentParser( add_help=False, prog="ins", @@ -291,12 +345,12 @@ def insider_activity(l_args, df_stock, s_ticker, s_start, s_interval): ) try: - ns_parser = parse_known_args_and_warn(parser, l_args) + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return url_market_business_insider = ( - f"https://markets.businessinsider.com/stocks/{s_ticker.lower()}-stock" + f"https://markets.businessinsider.com/stocks/{ticker.lower()}-stock" ) text_soup_market_business_insider = BeautifulSoup( requests.get( @@ -334,17 +388,17 @@ def insider_activity(l_args, df_stock, s_ticker, s_start, s_interval): df_insider = df_insider.set_index("Date") df_insider = df_insider.sort_index(ascending=True) - if s_start: - df_insider = df_insider[s_start:] + if start: + df_insider = df_insider[start:] # type: ignore _, ax = plt.subplots() - if s_interval == "1440min": - plt.plot(df_stock.index, df_stock["5. adjusted close"].values, lw=3) + if interval == "1440min": + plt.plot(stock.index, stock["5. adjusted close"].values, lw=3) else: # Intraday - plt.plot(df_stock.index, df_stock["4. close"].values, lw=3) + plt.plot(stock.index, stock["4. close"].values, lw=3) - plt.title(f"{s_ticker.upper()} (Time Series) and Price Target") + plt.title(f"{ticker.upper()} (Time Series) and Price Target") plt.xlabel("Time") plt.ylabel("Share Price ($)") @@ -354,7 +408,7 @@ def insider_activity(l_args, df_stock, s_ticker, s_start, s_interval): * float(row["Shares Traded"].replace(",", "")), axis=1, ) - plt.xlim(df_insider.index[0], df_stock.index[-1]) + plt.xlim(df_insider.index[0], stock.index[-1]) min_price, max_price = ax.get_ylim() price_range = max_price - min_price @@ -373,16 +427,16 @@ def insider_activity(l_args, df_stock, s_ticker, s_start, s_interval): for ind in ( df_insider[df_insider["Type"] == "Sell"].groupby(by=["Date"]).sum().index ): - if ind in df_stock.index: + if ind in stock.index: ind_dt = ind else: ind_dt = get_next_stock_market_days(ind, 1)[0] n_stock_price = 0 - if s_interval == "1440min": - n_stock_price = df_stock["5. adjusted close"][ind_dt] + if interval == "1440min": + n_stock_price = stock["5. adjusted close"][ind_dt] else: - n_stock_price = df_stock["4. close"][ind_dt] + n_stock_price = stock["4. close"][ind_dt] plt.vlines( x=ind_dt, @@ -402,16 +456,16 @@ def insider_activity(l_args, df_stock, s_ticker, s_start, s_interval): for ind in ( df_insider[df_insider["Type"] == "Buy"].groupby(by=["Date"]).sum().index ): - if ind in df_stock.index: + if ind in stock.index: ind_dt = ind else: ind_dt = get_next_stock_market_days(ind, 1)[0] n_stock_price = 0 - if s_interval == "1440min": - n_stock_price = df_stock["5. adjusted close"][ind_dt] + if interval == "1440min": + n_stock_price = stock["5. adjusted close"][ind_dt] else: - n_stock_price = df_stock["4. close"][ind_dt] + n_stock_price = stock["4. close"][ind_dt] plt.vlines( x=ind_dt, @@ -431,6 +485,10 @@ def insider_activity(l_args, df_stock, s_ticker, s_start, s_interval): 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() l_names = list() diff --git a/gamestonk_terminal/due_diligence/dd_controller.py b/gamestonk_terminal/due_diligence/dd_controller.py new file mode 100644 index 00000000000..14ecab8fc40 --- /dev/null +++ b/gamestonk_terminal/due_diligence/dd_controller.py @@ -0,0 +1,219 @@ +""" Due Diligence Controller """ +__docformat__ = "numpy" + +import argparse +from typing import List +from pandas.core.frame import DataFrame +from prompt_toolkit.completion import NestedCompleter + +from gamestonk_terminal.due_diligence import business_insider_view as bi_view +from gamestonk_terminal.due_diligence import financial_modeling_prep_view as fmp_view +from gamestonk_terminal.due_diligence import finviz_view as fvz_view +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 import feature_flags as gtff +from gamestonk_terminal.helper_funcs import get_flair +from gamestonk_terminal.menu import session + + +class DueDiligenceController: + """ Due Diligence Controller """ + + # Command choices + CHOICES = [ + "help", + "q", + "quit", + "red", + "short", + "rating", + "pt", + "est", + "ins", + "insider", + "news", + "analyst", + "warnings", + "sec", + ] + + def __init__(self, stock: DataFrame, ticker: str, start: str, interval: str): + """Constructor + + Parameters + ---------- + stock : DataFrame + Due diligence stock dataframe + ticker : str + Due diligence ticker symbol + start : str + Start date of the stock data + interval : str + Stock data interval + """ + self.stock = stock + self.ticker = ticker + self.start = start + self.interval = interval + self.dd_parser = argparse.ArgumentParser(add_help=False, prog="dd") + self.dd_parser.add_argument( + "cmd", + choices=self.CHOICES, + ) + + def print_help(self): + """ Print help """ + + intraday = (f"Intraday {self.interval}", "Daily")[self.interval == "1440min"] + + if self.start: + print( + f"\n{intraday} Stock: {self.ticker} (from {self.start.strftime('%Y-%m-%d')})" + ) + else: + print(f"\n{intraday} Stock: {self.ticker}") + + print("\nDue Diligence:") + print(" help show this fundamental analysis menu again") + print(" q quit this menu, and shows back to main menu") + print(" quit quit to abandon program") + print("") + print(" news latest news of the company [News API]") + print(" red gets due diligence from another user's post [Reddit]") + print(" analyst analyst prices and ratings of the company [Finviz]") + print( + " rating rating of the company from strong sell to strong buy [FMP]" + ) + print(" pt price targets over time [Business Insider]") + print( + " est quarter and year analysts earnings estimates [Business Insider]" + ) + print(" ins insider activity over time [Business Insider]") + print(" insider insider trading of the company [Finviz]") + print(" sec SEC filings [Market Watch]") + print(" short short interest [Quandl]") + print( + " warnings company warnings according to Sean Seah book [Market Watch]" + ) + 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.dd_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() + + 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_red(self, other_args: List[str]): + """ Process red command """ + r_view.due_diligence(other_args, self.ticker) + + def call_insider(self, other_args: List[str]): + """ Process insider command """ + fvz_view.insider(other_args, self.ticker) + + def call_news(self, other_args: List[str]): + """ Process news command """ + news_view.news(other_args, self.ticker) + + def call_analyst(self, other_args: List[str]): + """ Process analyst command """ + fvz_view.analyst(other_args, self.ticker) + + def call_pt(self, other_args: List[str]): + """ Process pt command """ + bi_view.price_target_from_analysts( + other_args, self.stock, self.ticker, self.start, self.interval + ) + + def call_est(self, other_args: List[str]): + """ Process est command """ + bi_view.estimates(other_args, self.ticker) + + def call_ins(self, other_args: List[str]): + """ Process ins command """ + bi_view.insider_activity( + other_args, self.stock, self.ticker, self.start, self.interval + ) + + def call_rating(self, other_args: List[str]): + """ Process rating command """ + fmp_view.rating(other_args, self.ticker) + + def call_warnings(self, other_args: List[str]): + """ Process rating command """ + mw_view.sean_seah_warnings(other_args, self.ticker) + + def call_sec(self, other_args: List[str]): + """ Process sec command """ + mw_view.sec_fillings(other_args, self.ticker) + + def call_short(self, other_args: List[str]): + """ Process short command """ + q_view.short_interest(other_args, self.ticker, self.start) + + +def menu(stock: DataFrame, ticker: str, start: str, interval: str): + """Due Diligence Menu + + Parameters + ---------- + stock : DataFrame + Due diligence stock dataframe + ticker : str + Due diligence ticker symbol + start : str + Start date of the stock data + interval : str + Stock data interval + """ + + dd_controller = DueDiligenceController(stock, ticker, start, interval) + dd_controller.call_help(None) + + while True: + # Get input command from user + if session and gtff.USE_PROMPT_TOOLKIT: + completer = NestedCompleter.from_nested_dict( + {c: None for c in dd_controller.CHOICES} + ) + + an_input = session.prompt( + f"{get_flair()} (dd)> ", + completer=completer, + ) + else: + an_input = input(f"{get_flair()} (dd)> ") + + try: + process_input = dd_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/gamestonk_terminal/due_diligence/dd_menu.py b/gamestonk_terminal/due_diligence/dd_menu.py deleted file mode 100644 index 94635e6d8c8..00000000000 --- a/gamestonk_terminal/due_diligence/dd_menu.py +++ /dev/null @@ -1,148 +0,0 @@ -import argparse - -from gamestonk_terminal.due_diligence import business_insider_api as bi_api -from gamestonk_terminal.due_diligence import financial_modeling_prep_api as fmp_api -from gamestonk_terminal.due_diligence import finviz_api as fvz_api -from gamestonk_terminal.due_diligence import market_watch_api as mw_api -from gamestonk_terminal.due_diligence import quandl_api as q_api -from gamestonk_terminal.due_diligence import reddit_api as r_api -from gamestonk_terminal.due_diligence import news_api -from gamestonk_terminal import feature_flags as gtff -from gamestonk_terminal.helper_funcs import get_flair -from gamestonk_terminal.menu import session -from prompt_toolkit.completion import NestedCompleter - - -def print_due_diligence(s_ticker, s_start, s_interval): - """ Print help """ - - s_intraday = (f"Intraday {s_interval}", "Daily")[s_interval == "1440min"] - - if s_start: - print(f"\n{s_intraday} Stock: {s_ticker} (from {s_start.strftime('%Y-%m-%d')})") - else: - print(f"\n{s_intraday} Stock: {s_ticker}") - - print("\nDue Diligence:") - print(" help show this fundamental analysis menu again") - print(" q quit this menu, and shows back to main menu") - print(" quit quit to abandon program") - print("") - print(" news latest news of the company [News API]") - print(" red gets due diligence from another user's post [Reddit]") - print(" analyst analyst prices and ratings of the company [Finviz]") - print(" rating rating of the company from strong sell to strong buy [FMP]") - print(" pt price targets over time [Business Insider]") - print( - " est quarter and year analysts earnings estimates [Business Insider]" - ) - print(" ins insider activity over time [Business Insider]") - print(" insider insider trading of the company [Finviz]") - print(" sec SEC filings [Market Watch]") - print(" short short interest [Quandl]") - print( - " warnings company warnings according to Sean Seah book [Market Watch]" - ) - print("") - return - - -def dd_menu(df_stock, s_ticker, s_start, s_interval): - - # Add list of arguments that the due diligence parser accepts - dd_parser = argparse.ArgumentParser(prog="dd", add_help=False) - choices = [ - "info", - "help", - "q", - "quit", - "red", - "short", - "rating", - "pt", - "est", - "ins", - "insider", - "news", - "analyst", - "warnings", - "sec", - ] - dd_parser.add_argument("cmd", choices=choices) - completer = NestedCompleter.from_nested_dict({c: None for c in choices}) - - print_due_diligence(s_ticker, s_start, s_interval) - - # Loop forever and ever - while True: - # Get input command from user - if session and gtff.USE_PROMPT_TOOLKIT: - as_input = session.prompt( - f"{get_flair()} (dd)> ", - completer=completer, - ) - else: - as_input = input(f"{get_flair()} (dd)> ") - - # Parse due diligence command of the list of possible commands - try: - (ns_known_args, l_args) = dd_parser.parse_known_args(as_input.split()) - - except SystemExit: - print("The command selected doesn't exist\n") - continue - - if ns_known_args.cmd == "help": - print_due_diligence(s_ticker, s_start, s_interval) - - elif ns_known_args.cmd == "q": - # Just leave the DD menu - return False - - elif ns_known_args.cmd == "quit": - # Abandon the program - return True - - # REDDIT API - elif ns_known_args.cmd == "red": - r_api.due_diligence(l_args, s_ticker) - - # FINVIZ API - elif ns_known_args.cmd == "insider": - fvz_api.insider(l_args, s_ticker) - - elif ns_known_args.cmd == "news": - news_api.news(l_args, s_ticker) - - elif ns_known_args.cmd == "analyst": - fvz_api.analyst(l_args, s_ticker) - - # BUSINESS INSIDER API - elif ns_known_args.cmd == "pt": - bi_api.price_target_from_analysts( - l_args, df_stock, s_ticker, s_start, s_interval - ) - - elif ns_known_args.cmd == "est": - bi_api.estimates(l_args, s_ticker) - - elif ns_known_args.cmd == "ins": - bi_api.insider_activity(l_args, df_stock, s_ticker, s_start, s_interval) - - # FINANCIAL MODELING PREP API - elif ns_known_args.cmd == "rating": - fmp_api.rating(l_args, s_ticker) - - # MARKET WATCH API - elif ns_known_args.cmd == "sec": - mw_api.sec_fillings(l_args, s_ticker) - - elif ns_known_args.cmd == "warnings": - mw_api.sean_seah_warnings(l_args, s_ticker) - - # QUANDL API - elif ns_known_args.cmd == "short": - q_api.short_interest(l_args, s_ticker, s_start) - - else: - print("Command not recognized!") diff --git a/gamestonk_terminal/due_diligence/financial_modeling_prep_api.py b/gamestonk_terminal/due_diligence/financial_modeling_prep_view.py similarity index 65% rename from gamestonk_terminal/due_diligence/financial_modeling_prep_api.py rename to gamestonk_terminal/due_diligence/financial_modeling_prep_view.py index 9163bf02149..4026649e2fe 100644 --- a/gamestonk_terminal/due_diligence/financial_modeling_prep_api.py +++ b/gamestonk_terminal/due_diligence/financial_modeling_prep_view.py @@ -1,10 +1,24 @@ +""" Financial Modeling Prep View """ +__docformat__ = "numpy" + import argparse +from typing import List import FundamentalAnalysis as fa # Financial Modeling Prep from gamestonk_terminal import config_terminal as cfg from gamestonk_terminal.helper_funcs import parse_known_args_and_warn -def rating(l_args, s_ticker): +def rating(other_args: List[str], ticker: str): + """Display ratings for a given ticker + + Parameters + ---------- + other_args : List[str] + argparse other args + ticker : str + Stock ticker + """ + parser = argparse.ArgumentParser( add_help=False, prog="rating", @@ -16,18 +30,18 @@ def rating(l_args, s_ticker): ) try: - ns_parser = parse_known_args_and_warn(parser, l_args) + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return - df_fa = fa.rating(s_ticker, cfg.API_KEY_FINANCIALMODELINGPREP) + df_fa = fa.rating(ticker, cfg.API_KEY_FINANCIALMODELINGPREP) print(df_fa) print("") except KeyError: print( - f"Financialmodelingprep.com is returning empty response the ticker {s_ticker}." + f"Financialmodelingprep.com is returning empty response the ticker {ticker}." ) print("") return diff --git a/gamestonk_terminal/due_diligence/finviz_api.py b/gamestonk_terminal/due_diligence/finviz_view.py similarity index 70% rename from gamestonk_terminal/due_diligence/finviz_api.py rename to gamestonk_terminal/due_diligence/finviz_view.py index 50b47b433ee..56a3eaa8a21 100644 --- a/gamestonk_terminal/due_diligence/finviz_api.py +++ b/gamestonk_terminal/due_diligence/finviz_view.py @@ -1,4 +1,8 @@ +""" FinViz View """ +__docformat__ = "numpy" + import argparse +from typing import List from colorama import Fore, Style import finviz import pandas as pd @@ -11,6 +15,19 @@ from gamestonk_terminal.helper_funcs import ( def category_color_red_green(val: str) -> str: + """Add color to analyst rating + + Parameters + ---------- + val : str + Analyst rating - Upgrade/Downgrade + + Returns + ------- + str + Analyst rating with color + """ + if val == "Upgrade": return Fore.GREEN + val + Style.RESET_ALL if val == "Downgrade": @@ -18,7 +35,16 @@ def category_color_red_green(val: str) -> str: return val -def insider(l_args, s_ticker): +def insider(other_args: List[str], ticker: str): + """Display insider activity for a given stock ticker + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-n", "10"] + ticker : str + Stock ticker + """ parser = argparse.ArgumentParser( add_help=False, prog="insider", @@ -39,11 +65,11 @@ def insider(l_args, s_ticker): ) try: - ns_parser = parse_known_args_and_warn(parser, l_args) + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return - d_finviz_insider = finviz.get_insider(s_ticker) + d_finviz_insider = finviz.get_insider(ticker) df_fa = pd.DataFrame.from_dict(d_finviz_insider) df_fa.set_index("Date", inplace=True) df_fa = df_fa[ @@ -68,7 +94,17 @@ def insider(l_args, s_ticker): return -def news(l_args, s_ticker): +def news(other_args: List[str], ticker: str): + """Display news for a given stock ticker + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-n", "10"] + ticker : str + Stock ticker + """ + parser = argparse.ArgumentParser( add_help=False, prog="news", @@ -88,11 +124,11 @@ def news(l_args, s_ticker): ) try: - ns_parser = parse_known_args_and_warn(parser, l_args) + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return - d_finviz_news = finviz.get_news(s_ticker) + d_finviz_news = finviz.get_news(ticker) i = 0 for s_news_title, s_news_link in {*d_finviz_news}: print(f"-> {s_news_title}") @@ -110,9 +146,22 @@ def news(l_args, s_ticker): return -def analyst_df(s_ticker: str) -> DataFrame: +def analyst_df(ticker: str) -> DataFrame: + """[summary] + + Parameters + ---------- + ticker : str + Stock ticker + + Returns + ------- + DataFrame + [description] + """ + try: - d_finviz_analyst_price = finviz.get_analyst_price_targets(s_ticker) + d_finviz_analyst_price = finviz.get_analyst_price_targets(ticker) df_fa = pd.DataFrame.from_dict(d_finviz_analyst_price) df_fa.set_index("date", inplace=True) except Exception as e: @@ -122,7 +171,17 @@ def analyst_df(s_ticker: str) -> DataFrame: return df_fa -def analyst(l_args, s_ticker): +def analyst(other_args, ticker): + """Display analyst ratings + + Parameters + ---------- + other_args : [type] + argparse other args + ticker : [type] + Stock ticker + """ + parser = argparse.ArgumentParser( add_help=False, prog="analyst", @@ -144,11 +203,11 @@ def analyst(l_args, s_ticker): ) try: - ns_parser = parse_known_args_and_warn(parser, l_args) + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return - df_fa = analyst_df(s_ticker) + df_fa = analyst_df(ticker) if ns_parser.n_color == 1: df_fa["category"] = df_fa["category"].apply(category_color_red_green) diff --git a/gamestonk_terminal/due_diligence/market_watch_api.py b/gamestonk_terminal/due_diligence/market_watch_view.py similarity index 92% rename from gamestonk_terminal/due_diligence/market_watch_api.py rename to gamestonk_terminal/due_diligence/market_watch_view.py index f9cfd054983..7a080347cb1 100644 --- a/gamestonk_terminal/due_diligence/market_watch_api.py +++ b/gamestonk_terminal/due_diligence/market_watch_view.py @@ -1,4 +1,8 @@ +""" Market Watch View """ +__docformat__ = "numpy" + import argparse +from typing import List import re import requests import pandas as pd @@ -13,7 +17,17 @@ from gamestonk_terminal.helper_funcs import ( ) -def sec_fillings(l_args, s_ticker): +def sec_fillings(other_args: List[str], ticker: str): + """Display SEC filings for a given stock ticker + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-n", "10"] + ticker : str + Stock ticker + """ + parser = argparse.ArgumentParser( add_help=False, prog="sec", @@ -34,13 +48,13 @@ def sec_fillings(l_args, s_ticker): ) try: - ns_parser = parse_known_args_and_warn(parser, l_args) + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return pd.set_option("display.max_colwidth", None) - url_financials = f"https://www.marketwatch.com/investing/stock/{s_ticker}/financials/secfilings" + url_financials = f"https://www.marketwatch.com/investing/stock/{ticker}/financials/secfilings" text_soup_financials = BeautifulSoup( requests.get(url_financials, headers={"User-Agent": get_user_agent()}).text, @@ -60,7 +74,7 @@ def sec_fillings(l_args, s_ticker): l_financials_info.extend(a_financials[5:-1]) l_financials_info.append(financials_info.a["href"]) # Append data values to financials - df_financials.loc[len(df_financials.index)] = l_financials_info + df_financials.loc[len(df_financials.index)] = l_financials_info # type: ignore if "Filing Date" in a_financials: l_financials_header = [a_financials[2]] @@ -72,7 +86,7 @@ def sec_fillings(l_args, s_ticker): b_ready_to_process_info = True # Set Filing Date as index - df_financials = df_financials.set_index("Filing Date") + df_financials = df_financials.set_index("Filing Date") # type: ignore print(df_financials.head(n=ns_parser.n_num).to_string()) print("") @@ -83,7 +97,17 @@ def sec_fillings(l_args, s_ticker): return -def sean_seah_warnings(l_args, s_ticker): +def sean_seah_warnings(other_args: List[str], ticker: str): + """Display Sean Seah warnings + + Parameters + ---------- + other_args : List[str] + argparse other args + ticker : str + Stock ticker + """ + parser = argparse.ArgumentParser( add_help=False, prog="warnings", @@ -114,7 +138,7 @@ def sean_seah_warnings(l_args, s_ticker): ) try: - ns_parser = parse_known_args_and_warn(parser, l_args) + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return @@ -129,7 +153,7 @@ def sean_seah_warnings(l_args, s_ticker): # From INCOME STATEMENT, get: 'EPS (Basic)', 'Net Income', 'Interest Expense', 'EBITDA' url_financials = ( - f"https://www.marketwatch.com/investing/stock/{s_ticker}/financials/income" + f"https://www.marketwatch.com/investing/stock/{ticker}/financials/income" ) text_soup_financials = BeautifulSoup( requests.get(url_financials, headers={"User-Agent": get_user_agent()}).text, @@ -173,7 +197,7 @@ def sean_seah_warnings(l_args, s_ticker): ] # From BALANCE SHEET, get: 'Liabilities & Shareholders\' Equity', 'Long-Term Debt' - url_financials = f"https://www.marketwatch.com/investing/stock/{s_ticker}/financials/balance-sheet" + url_financials = f"https://www.marketwatch.com/investing/stock/{ticker}/financials/balance-sheet" text_soup_financials = BeautifulSoup( requests.get(url_financials, headers={"User-Agent": get_user_agent()}).text, "lxml", diff --git a/gamestonk_terminal/due_diligence/news_api.py b/gamestonk_terminal/due_diligence/news_view.py similarity index 78% rename from gamestonk_terminal/due_diligence/news_api.py rename to gamestonk_terminal/due_diligence/news_view.py index 7d61da02925..9b82d67caae 100644 --- a/gamestonk_terminal/due_diligence/news_api.py +++ b/gamestonk_terminal/due_diligence/news_view.py @@ -1,4 +1,8 @@ +""" News View """ +__docformat__ = "numpy" + import argparse +from typing import List from datetime import datetime, timedelta import requests from gamestonk_terminal import config_terminal as cfg @@ -8,7 +12,17 @@ from gamestonk_terminal.helper_funcs import ( ) -def news(l_args, s_ticker): +def news(other_args: List[str], ticker: str): + """Display news 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="news", @@ -28,14 +42,14 @@ def news(l_args, s_ticker): ) try: - ns_parser = parse_known_args_and_warn(parser, l_args) + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return s_from = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d") response = requests.get( - f"https://newsapi.org/v2/everything?q={s_ticker}&from={s_from}" + f"https://newsapi.org/v2/everything?q={ticker}&from={s_from}" f"&sortBy=publishedAt&language=en&apiKey={cfg.API_NEWS_TOKEN}", ) @@ -45,7 +59,7 @@ def news(l_args, s_ticker): else: print( - f"{response.json()['totalResults']} news articles from {s_ticker} were found since {s_from}\n" + f"{response.json()['totalResults']} news articles from {ticker} were found since {s_from}\n" ) for idx, article in enumerate(response.json()["articles"]): diff --git a/gamestonk_terminal/due_diligence/quandl_api.py b/gamestonk_terminal/due_diligence/quandl_view.py similarity index 77% rename from gamestonk_terminal/due_diligence/quandl_api.py rename to gamestonk_terminal/due_diligence/quandl_view.py index 7d4254a9f6d..b0ca0f09f67 100644 --- a/gamestonk_terminal/due_diligence/quandl_api.py +++ b/gamestonk_terminal/due_diligence/quandl_view.py @@ -1,7 +1,11 @@ +""" Quandl View """ +__docformat__ = "numpy" + import argparse +from typing import List import quandl from matplotlib import pyplot as plt -import matplotlib.ticker as ticker +import matplotlib.ticker import pandas as pd from gamestonk_terminal.helper_funcs import ( check_positive, @@ -11,7 +15,19 @@ from gamestonk_terminal.helper_funcs import ( from gamestonk_terminal import config_terminal as cfg -def short_interest(l_args, s_ticker, s_start): +def short_interest(other_args: List[str], ticker: str, start: str): + """Display short interest for a given ticker and a given start date + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-d", "10"] + ticker : str + Stock ticker + start : str + Start date of the stock data + """ + parser = argparse.ArgumentParser( add_help=False, prog="short", @@ -40,18 +56,18 @@ def short_interest(l_args, s_ticker, s_start): ) try: - ns_parser = parse_known_args_and_warn(parser, l_args) + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return quandl.ApiConfig.api_key = cfg.API_KEY_QUANDL if ns_parser.b_nyse: - df_short_interest = quandl.get(f"FINRA/FNYX_{s_ticker}") + df_short_interest = quandl.get(f"FINRA/FNYX_{ticker}") else: - df_short_interest = quandl.get(f"FINRA/FNSQ_{s_ticker}") + df_short_interest = quandl.get(f"FINRA/FNSQ_{ticker}") - df_short_interest = df_short_interest[s_start:] + df_short_interest = df_short_interest[start:] # type: ignore df_short_interest.columns = [ "".join( " " + char if char.isupper() else char.strip() for char in idx @@ -77,18 +93,18 @@ def short_interest(l_args, s_ticker, s_start): ax.set_ylabel("Shares") ax.set_xlabel("Date") - if s_start: + if start: ax.set_title( - f"{('NASDAQ', 'NYSE')[ns_parser.b_nyse]} Short Interest on {s_ticker} from {s_start.date()}" + f"{('NASDAQ', 'NYSE')[ns_parser.b_nyse]} Short Interest on {ticker} from {start.date()}" # type: ignore ) else: ax.set_title( - f"{('NASDAQ', 'NYSE')[ns_parser.b_nyse]} Short Interest on {s_ticker}" + f"{('NASDAQ', 'NYSE')[ns_parser.b_nyse]} Short Interest on {ticker}" ) ax.legend(labels=["Short Volume", "Total Volume"]) ax.tick_params(axis="both", which="major") - ax.yaxis.set_major_formatter(ticker.EngFormatter()) + ax.yaxis.set_major_formatter(matplotlib.ticker.EngFormatter()) ax_twin = ax.twinx() ax_twin.tick_params(axis="y", colors="green") ax_twin.set_ylabel("Percentage of Volume Shorted", color="green") @@ -98,7 +114,9 @@ def short_interest(l_args, s_ticker, s_start): color="green", ) ax_twin.tick_params(axis="y", which="major", color="green") - ax_twin.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.0f%%")) + ax_twin.yaxis.set_major_formatter( + matplotlib.ticker.FormatStrFormatter("%.0f%%") + ) plt.xlim([df_short_interest.index[0], df_short_interest.index[-1]]) df_short_interest["% of Volume Shorted"] = df_short_interest[ diff --git a/gamestonk_terminal/due_diligence/reddit_api.py b/gamestonk_terminal/due_diligence/reddit_view.py similarity index 88% rename from gamestonk_terminal/due_diligence/reddit_api.py rename to gamestonk_terminal/due_diligence/reddit_view.py index f305abacef7..5c536add200 100644 --- a/gamestonk_terminal/due_diligence/reddit_api.py +++ b/gamestonk_terminal/due_diligence/reddit_view.py @@ -1,4 +1,8 @@ +""" Reddit View """ +__docformat__ = "numpy" + import argparse +from typing import List from datetime import datetime, timedelta from psaw import PushshiftAPI import praw @@ -7,7 +11,17 @@ from gamestonk_terminal import config_terminal as cfg from gamestonk_terminal.reddit_helpers import print_and_record_reddit_post -def due_diligence(l_args, s_ticker): +def due_diligence(other_args: List[str], ticker: str): + """Display Reddit due diligence data for a given ticker + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-l", "5"] + ticker : str + Stock ticker + """ + parser = argparse.ArgumentParser( add_help=False, prog="red", @@ -47,7 +61,7 @@ def due_diligence(l_args, s_ticker): ) try: - ns_parser = parse_known_args_and_warn(parser, l_args) + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return @@ -83,9 +97,9 @@ def due_diligence(l_args, s_ticker): ] submissions = psaw_api.search_submissions( - after=int(n_ts_after), subreddit=l_sub_reddits, q=s_ticker, filter=["id"] + after=int(n_ts_after), subreddit=l_sub_reddits, q=ticker, filter=["id"] ) - d_submission = {} + d_submission = {} # type: ignore n_flair_posts_found = 0 while True: submission = next(submissions, None) diff --git a/gamestonk_terminal/feature_flags.py b/gamestonk_terminal/feature_flags.py index 3eaebcd2bb2..5c4910a1ab2 100644 --- a/gamestonk_terminal/feature_flags.py +++ b/gamestonk_terminal/feature_flags.py @@ -16,6 +16,9 @@ USE_PROMPT_TOOLKIT = strtobool(os.getenv("GTFF_USE_PROMPT_TOOLKIT", "False")) # Enable Prediction features ENABLE_PREDICT = strtobool(os.getenv("GTFF_ENABLE_PREDICT", "False")) +# Enable FB Prophet +ENABLE_FBPROPHET = strtobool(os.getenv("GTFF_ENABLE_FBPROPHET", "False")) + # Enable plot autoscaling USE_PLOT_AUTOSCALING = strtobool(os.getenv("GTFF_USE_PLOT_AUTOSCALING", "False")) diff --git a/gamestonk_terminal/main_helper.py b/gamestonk_terminal/main_helper.py index 27c0cacdb09..40c8f6714ba 100644 --- a/gamestonk_terminal/main_helper.py +++ b/gamestonk_terminal/main_helper.py @@ -1,10 +1,12 @@ import argparse from sys import stdout +from datetime import datetime, timedelta import matplotlib.pyplot as plt import pandas as pd from alpha_vantage.timeseries import TimeSeries import mplfinance as mpf import yfinance as yf +import pytz from gamestonk_terminal.helper_funcs import ( valid_date, @@ -58,6 +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 optimal portfolio weights from pyportfolioopt" + ) if s_ticker: print( @@ -117,7 +122,9 @@ def load(l_args, s_ticker, s_start, s_interval, df_stock): parser = argparse.ArgumentParser( add_help=False, prog="load", - description=""" Load a stock in order to perform analysis.""", + description="Load stock ticker to perform analysis on. When the data source is 'yf', an Indian ticker can be" + " loaded by using '.NS' at the end, e.g. 'SBIN.NS'. See available market in" + " https://help.yahoo.com/kb/exchanges-data-providers-yahoo-finance-sln2310.html.", ) parser.add_argument( "-t", @@ -151,8 +158,15 @@ def load(l_args, s_ticker, s_start, s_interval, df_stock): dest="source", type=check_sources, default="yf", - help="Source of historical data. Intraday: Only 'av' available." - "Daily: 'yf' and 'av' available.", + help="Source of historical data. 'yf' and 'av' available.", + ) + parser.add_argument( + "-p", + "--prepost", + action="store_true", + default=False, + dest="b_prepost", + help="Pre/After market hours. Only works for 'yf' source, and intraday data", ) try: @@ -229,9 +243,54 @@ def load(l_args, s_ticker, s_start, s_interval, df_stock): df_stock = df_stock[ns_parser.s_start_date :] elif ns_parser.source == "yf": - print( - "Unfortunately, yahoo finance historical data doesn't contain intraday prices" + s_int = s_interval[:-2] + + d_granularity = {"1m": 6, "5m": 59, "15m": 59, "30m": 59, "60m": 729} + + s_start_dt = datetime.utcnow() - timedelta(days=d_granularity[s_int]) + s_date_start = s_start_dt.strftime("%Y-%m-%d") + + s_start = pytz.utc.localize(s_start_dt) + + if s_start_dt > ns_parser.s_start_date: + print( + f"Using Yahoo Finance with granularity {s_int} the starting date is set to: {s_date_start}\n" + ) + + df_stock = yf.download( + ns_parser.s_ticker, + start=s_date_start, + progress=False, + interval=s_int, + prepost=ns_parser.b_prepost, + ) + + else: + df_stock = yf.download( + ns_parser.s_ticker, + start=ns_parser.s_start_date.strftime("%Y-%m-%d"), + progress=False, + interval=s_int, + prepost=ns_parser.b_prepost, + ) + + # Check that a stock was successfully retrieved + if df_stock.empty: + print("") + return [s_ticker, s_start, s_interval, df_stock] + + df_stock = df_stock.rename( + columns={ + "Open": "1. open", + "High": "2. high", + "Low": "3. low", + "Close": "4. close", + "Adj Close": "5. adjusted close", + "Volume": "6. volume", + } ) + df_stock.index.name = "date" + return [s_ticker, s_start, s_interval, df_stock] except Exception as e: @@ -369,24 +428,6 @@ def view(l_args, s_ticker, s_start, s_interval, df_stock): type_candles = lett_to_num(ns_parser.type) - try: - ts = TimeSeries(key=cfg.API_KEY_ALPHAVANTAGE, output_format="pandas") - # Daily - if s_interval == "1440min": - # pylint: disable=unbalanced-tuple-unpacking - df_stock, _ = ts.get_daily_adjusted(symbol=s_ticker, outputsize="full") - # Intraday - else: - # pylint: disable=unbalanced-tuple-unpacking - df_stock, _ = ts.get_intraday( - symbol=s_ticker, outputsize="full", interval=s_interval - ) - - except Exception as e: - print(e) - print("Either the ticker or the API_KEY are invalids. Try again!") - return - df_stock.sort_index(ascending=True, inplace=True) # Slice dataframe from the starting date YYYY-MM-DD selected diff --git a/gamestonk_terminal/options/op_controller.py b/gamestonk_terminal/options/op_controller.py index 9459833b180..e373ba1f677 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", "vcalls", "vputs"] - 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,9 @@ 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(" vcalls calls volume + open interest plot") + print(" vputs puts volume + open interest plot") print("") return @@ -121,26 +123,47 @@ class OptionsController: """Process Quit command - quit the program.""" return True - def call_volume(self, _): - """Process volume command.""" - volume.volume_graph( - self.raw_data_options, - self.ticker, - self.expiry_date, - volume_percentile_threshold=60, - ) - print("") - 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.plot_volume_open_interest( + other_args, + self.ticker, + self.expiry_date, + self.last_adj_close_price, + self.options.calls, + self.options.puts, + ) -def menu(ticker: str): - """Options info Menu.""" + def call_vcalls(self, other_args: List[str]): + """Process vcalls command.""" + volume_view.plot_calls_volume_open_interest( + other_args, + self.ticker, + self.expiry_date, + 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.expiry_date, + self.last_adj_close_price, + self.options.puts, + ) + + +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..a20e4cafc54 --- /dev/null +++ b/gamestonk_terminal/options/volume_view.py @@ -0,0 +1,463 @@ +""" 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 + +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 plot_volume_open_interest( + 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 + """ + df_calls = pd.DataFrame() + df_puts = pd.DataFrame() + max_pain = 0 + + 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"]) + + 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, + exp_date: 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 + exp_date : str + Expiry date of the option + 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] + """, + ) + 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, _, _ = get_calls_puts_maxpain( + op_calls, pd.DataFrame(), last_adj_close_price + ) + + if ns_parser.min_sp == -1: + min_strike = 0.75 * last_adj_close_price + else: + min_strike = ns_parser.min_sp + + if ns_parser.max_sp == -1: + max_strike = 1.25 * last_adj_close_price + else: + max_strike = ns_parser.max_sp + + 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(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() + plt.show() + + print("") + + except Exception as e: + print(e, "\n") + return + + +def plot_puts_volume_open_interest( + other_args: List[str], + ticker: str, + exp_date: 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 + exp_date : str + Expiry date of the option + 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] + """, + ) + 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_puts, _ = get_calls_puts_maxpain( + pd.DataFrame(), op_puts, last_adj_close_price + ) + + if ns_parser.min_sp == -1: + min_strike = 0.75 * last_adj_close_price + else: + min_strike = ns_parser.min_sp + + if ns_parser.max_sp == -1: + max_strike = 1.25 * last_adj_close_price + else: + max_strike = ns_parser.max_sp + + 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(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() + plt.show() + + print("") + + except Exception as e: + print(e, "\n") + return diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md new file mode 100644 index 00000000000..bf96cf77629 --- /dev/null +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -0,0 +1,169 @@ +# Portfolio Optimization + +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 selected tickers to the menu to be considered + +```` +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. + +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 + +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 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] +```` +* -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. + +### 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 +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. + +##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. \ No newline at end of file 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/optimizer_helper.py b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py new file mode 100644 index 00000000000..3ca81f396a2 --- /dev/null +++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py @@ -0,0 +1,226 @@ +""" Portfolio Optimization Helper Functions""" +__docformat__ = "numpy" + +import argparse +from typing import List +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 +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 + +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: + """ + + 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, 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"] + 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 + + +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 + """ + if not weights: + return + 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] + ) + " %" + print(pd.DataFrame(weight_df["weight"])) + else: + print(weight_df) + + +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: float): + """ + 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 + """ + plt.close("all") + if not weights: + return + + 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) + + 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], + normalize=True, + shadow=True, + ) + else: + 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): + if sizes[i] / total_size > 0.05: + a.set_text(f"{sizes[i]:.2f}") + else: + a.set_text("") + + plt.axis("equal") + + plt.legend( + wedges, + leg_labels, + title="Stocks", + loc="center left", + bbox_to_anchor=(0.85, 0, 0.5, 1), + ) + + plt.setp(autotexts, size=8, weight="bold") + + 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("") + + +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 new file mode 100644 index 00000000000..e9d74a76138 --- /dev/null +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -0,0 +1,337 @@ +""" Portfolio Optimization Functions""" +__docformat__ = "numpy" + +import argparse +from typing import List +import matplotlib.pyplot as plt +import numpy as np +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.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 + +period_choices = [ + "1d", + "5d", + "1mo", + "3mo", + "6mo", + "1y", + "2y", + "5y", + "10y", + "ytd", + "max", +] + + +def equal_weight(stocks: List[str], other_args: List[str]): + """ + Equally weighted portfolio, where weight = 1/# of stocks + + Parameters + ---------- + 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="equal_weight", + description="Return equally weighted portfolio holdings", + ) + + parser.add_argument( + "-v", + "--value", + default=1, + type=float, + dest="value", + help="Amount to allocate to portfolio", + ) + 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 + values = {} + 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, "equal", 0) + if n_stocks >= 1: + print("Equal Weight Portfolio: ") + display_weights(values) + print("") + + except Exception as e: + print(e) + print("") + + +def property_weighting(stocks: List[str], property_type: str, other_args: List[str]): + """ + Property weighted portfolio where each weight is the relative fraction. + 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 + + Returns + ------- + 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="Amount to allocate to portfolio", + ) + parser.add_argument( + "--pie", + action="store_true", + dest="pie", + default=False, + help="Display a pie chart for weights", + ) + weights = {} + prop = {} + prop_sum = 0 + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + for stock in 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(): + weights[k] = round(v / prop_sum, 5) * ns_parser.value + + 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: + display_weights(weights) + print("") + + except Exception as e: + print(e) + print("") + return + + +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() + } + val = 0 + 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() + } + val = 0 + 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() + } + 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} %") + + else: + raise ValueError("EF Method not found") + + if ns_parser.pie: + pie_chart_weights(weights, port_type, val) + + 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( + "-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 + 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) + 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("") diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py new file mode 100644 index 00000000000..df2f8a55fa6 --- /dev/null +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -0,0 +1,206 @@ +"""Portfolio Optimization Controller Module""" +__docformat__ = "numpy" + + +import argparse +from typing import List +import matplotlib.pyplot as plt +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 + + +class PortfolioOptimization: + + CHOICES = [ + "help", + "q", + "quit", + "select", + "add", + "equal_weight", + "mkt_cap", + "div_yield", + "max_sharpe", + "min_vol", + "eff_risk", + "eff_ret", + "show_ef", + "yolo", + ] + + 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 = list(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: List[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(" select overwrite current tickers with new tickers") + print("") + print("Optimization:") + 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 + + 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_select(self, other_args: List[str]): + 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_mkt_cap(self, other_args: List[str]): + po_api.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_max_sharpe(self, other_args: List[str]): + po_api.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_eff_risk(self, other_args: List[str]): + po_api.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_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]): + """ 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: + + 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 + tickers = set(self.tickers) + for ticker in ns_parser.add_tickers: + tickers.add(ticker) + self.tickers = list(tickers) + + 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 = [] + plt.close("all") + 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/gamestonk_terminal/prediction_techniques/pred_controller.py b/gamestonk_terminal/prediction_techniques/pred_controller.py new file mode 100644 index 00000000000..8d2bdb4b2be --- /dev/null +++ b/gamestonk_terminal/prediction_techniques/pred_controller.py @@ -0,0 +1,207 @@ +import argparse +from typing import List +import pandas as pd +from datetime import datetime +import matplotlib.pyplot as plt +from gamestonk_terminal import feature_flags as gtff +from gamestonk_terminal.helper_funcs import get_flair +from gamestonk_terminal.menu import session +from gamestonk_terminal.prediction_techniques import ( + arima, + ets, + knn, + neural_networks, + regression, + sma, +) +from prompt_toolkit.completion import NestedCompleter + + +class PredictionTechniquesController: + """Prediction Techniques Controller class""" + + # Command choices + CHOICES = [ + "help", + "q", + "quit", + "sma", + "ets", + "knn", + "linear", + "quadratic", + "cubic", + "regression", + "arima", + "mlp", + "rnn", + "lstm", + ] + + if gtff.ENABLE_FBPROPHET: + CHOICES.append("prophet") + + def __init__( + self, + stock: pd.DataFrame, + ticker: str, + start: datetime, + interval: str, + ): + """Constructor""" + self.stock = stock + self.ticker = ticker + self.start = start + self.interval = interval + self.pred_parser = argparse.ArgumentParser(add_help=False, prog="pred") + self.pred_parser.add_argument( + "cmd", + choices=self.CHOICES, + ) + + @staticmethod + def print_help(self): + """ Print help """ + + s_intraday = (f"Intraday {self.interval}", "Daily")[self.interval == "1440min"] + + if self.start: + print( + f"\n{s_intraday} Stock: {self.ticker} (from {self.start.strftime('%Y-%m-%d')})" + ) + else: + print(f"\n{s_intraday} Stock: {self.ticker}") + + print("\nPrediction Techniques:") + print(" help show this prediction techniques menu again") + print(" q quit this menu, and shows back to main menu") + print(" quit quit to abandon program") + print("") + print(" sma simple moving average") + print(" ets exponential smoothing (e.g. Holt-Winters)") + print(" knn k-Nearest Neighbors") + print(" linear linear regression (polynomial 1)") + print(" quadratic quadratic regression (polynomial 2)") + print(" cubic cubic regression (polynomial 3)") + print(" regression regression (other polynomial)") + print(" arima autoregressive integrated moving average") + print(" mlp MultiLayer Perceptron") + print(" rnn Recurrent Neural Network") + print(" lstm Long-Short Term Memory") + if gtff.ENABLE_FBPROPHET: + print(" prophet Facebook's prophet prediction") + 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.pred_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) + + 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_sma(self, other_args: List[str]): + """Process sma command""" + sma.simple_moving_average(other_args, self.ticker, self.stock) + + def call_ets(self, other_args: List[str]): + """Process ets command""" + ets.exponential_smoothing(other_args, self.ticker, self.stock) + + def call_knn(self, other_args: List[str]): + """Process knn command""" + knn.k_nearest_neighbors(other_args, self.ticker, self.stock) + + def call_linear(self, other_args: List[str]): + """Process linear command""" + regression.regression(other_args, self.ticker, self.stock, regression.LINEAR) + + def call_quadratic(self, other_args: List[str]): + """Process quadratic command""" + regression.regression(other_args, self.ticker, self.stock, regression.QUADRATIC) + + def call_cubic(self, other_args: List[str]): + """Process cubic command""" + regression.regression(other_args, self.ticker, self.stock, regression.CUBIC) + + def call_regression(self, other_args: List[str]): + """Process regression command""" + regression.regression( + other_args, self.ticker, self.stock, regression.USER_INPUT + ) + + def call_arima(self, other_args: List[str]): + """Process arima command""" + arima.arima(other_args, self.ticker, self.stock) + + def call_mlp(self, other_args: List[str]): + """Process mlp command""" + neural_networks.mlp(other_args, self.ticker, self.stock) + + def call_rnn(self, other_args: List[str]): + """Process rnn command""" + neural_networks.rnn(other_args, self.ticker, self.stock) + + def call_lstm(self, other_args: List[str]): + """Process lstm command""" + neural_networks.lstm(other_args, self.ticker, self.stock) + + if gtff.ENABLE_FBPROPHET: + + def call_prophet(self, other_args: List[str]): + """Process prophet command""" + from gamestonk_terminal.prediction_techniques import fbprophet + + fbprophet.fbprophet(other_args, self.ticker, self.stock) + + +def menu(stock: pd.DataFrame, ticker: str, start: datetime, interval: str): + """Comparison Analysis Menu""" + + pred_controller = PredictionTechniquesController(stock, ticker, start, interval) + pred_controller.call_help(None) + + while True: + # Get input command from user + if session and gtff.USE_PROMPT_TOOLKIT: + completer = NestedCompleter.from_nested_dict( + {c: None for c in pred_controller.CHOICES} + ) + an_input = session.prompt( + f"{get_flair()} (pred)> ", + completer=completer, + ) + else: + an_input = input(f"{get_flair()} (pred)> ") + + try: + plt.close("all") + + process_input = pred_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/gamestonk_terminal/prediction_techniques/pred_menu.py b/gamestonk_terminal/prediction_techniques/pred_menu.py deleted file mode 100644 index 31460d87ae7..00000000000 --- a/gamestonk_terminal/prediction_techniques/pred_menu.py +++ /dev/null @@ -1,145 +0,0 @@ -import argparse - -import matplotlib.pyplot as plt -from gamestonk_terminal import feature_flags as gtff -from gamestonk_terminal.helper_funcs import get_flair -from gamestonk_terminal.menu import session -from gamestonk_terminal.prediction_techniques import ( - arima, - ets, - fbprophet, - knn, - neural_networks, - regression, - sma, -) -from prompt_toolkit.completion import NestedCompleter - - -def print_prediction(s_ticker, s_start, s_interval): - """ Print help """ - - s_intraday = (f"Intraday {s_interval}", "Daily")[s_interval == "1440min"] - - if s_start: - print(f"\n{s_intraday} Stock: {s_ticker} (from {s_start.strftime('%Y-%m-%d')})") - else: - print(f"\n{s_intraday} Stock: {s_ticker}") - - print("\nPrediction Techniques:") - print(" help show this prediction techniques menu again") - print(" q quit this menu, and shows back to main menu") - print(" quit quit to abandon program") - print("") - print(" sma simple moving average") - print(" ets exponential smoothing (e.g. Holt-Winters)") - print(" knn k-Nearest Neighbors") - print(" linear linear regression (polynomial 1)") - print(" quadratic quadratic regression (polynomial 2)") - print(" cubic cubic regression (polynomial 3)") - print(" regression regression (other polynomial)") - print(" arima autoregressive integrated moving average") - print(" prophet Facebook's prophet prediction") - print(" mlp MultiLayer Perceptron") - print(" rnn Recurrent Neural Network") - print(" lstm Long-Short Term Memory") - print("") - - -def pred_menu(df_stock, s_ticker, s_start, s_interval): - - # Add list of arguments that the prediction techniques parser accepts - pred_parser = argparse.ArgumentParser(prog="pred", add_help=False) - choices = [ - "help", - "q", - "quit", - "sma", - "ets", - "knn", - "linear", - "quadratic", - "cubic", - "regression", - "arima", - "prophet", - "mlp", - "rnn", - "lstm", - ] - pred_parser.add_argument("cmd", choices=choices) - completer = NestedCompleter.from_nested_dict({c: None for c in choices}) - - print_prediction(s_ticker, s_start, s_interval) - - # Loop forever and ever - while True: - # Get input command from user - if session and gtff.USE_PROMPT_TOOLKIT: - as_input = session.prompt( - f"{get_flair()} (pred)> ", - completer=completer, - ) - else: - as_input = input(f"{get_flair()} (pred)> ") - - # Images are non blocking - allows to close them if we type other command - plt.close("all") - - # Parse prediction techniques command of the list of possible commands - try: - (ns_known_args, l_args) = pred_parser.parse_known_args(as_input.split()) - - except SystemExit: - print("The command selected doesn't exist\n") - continue - - if ns_known_args.cmd == "help": - print_prediction(s_ticker, s_start, s_interval) - - elif ns_known_args.cmd == "q": - # Just leave the FA menu - return False - - elif ns_known_args.cmd == "quit": - # Abandon the program - return True - - elif ns_known_args.cmd == "sma": - sma.simple_moving_average(l_args, s_ticker, df_stock) - - elif ns_known_args.cmd == "ets": - ets.exponential_smoothing(l_args, s_ticker, df_stock) - - elif ns_known_args.cmd == "knn": - knn.k_nearest_neighbors(l_args, s_ticker, df_stock) - - elif ns_known_args.cmd == "linear": - regression.regression(l_args, s_ticker, df_stock, regression.LINEAR) - - elif ns_known_args.cmd == "quadratic": - regression.regression(l_args, s_ticker, df_stock, regression.QUADRATIC) - - elif ns_known_args.cmd == "cubic": - regression.regression(l_args, s_ticker, df_stock, regression.CUBIC) - - elif ns_known_args.cmd == "regression": - regression.regression(l_args, s_ticker, df_stock, regression.USER_INPUT) - - elif ns_known_args.cmd == "arima": - arima.arima(l_args, s_ticker, df_stock) - - elif ns_known_args.cmd == "prophet": - fbprophet.fbprophet(l_args, s_ticker, df_stock) - - elif ns_known_args.cmd == "mlp": - neural_networks.mlp(l_args, s_ticker, df_stock) - - elif ns_known_args.cmd == "rnn": - neural_networks.rnn(l_args, s_ticker, df_stock) - - elif ns_known_args.cmd == "lstm": - neural_networks.lstm(l_args, s_ticker, df_stock) - - else: - print("Command not recognized!") 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..0fe364750b0 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,10 @@ class ScreenerController: """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) + def menu(): """Screener Menu""" diff --git a/notebooks/templates/due_diligence.ipynb b/notebooks/templates/due_diligence.ipynb index cbbdc235398..09f4d95510f 100644 --- a/notebooks/templates/due_diligence.ipynb +++ b/notebooks/templates/due_diligence.ipynb @@ -68,7 +68,7 @@ "outputs": [], "source": [ "from gamestonk_terminal.technical_analysis import trendline_api as trend\n", - "from gamestonk_terminal.due_diligence import finviz_api as finviz" + "from gamestonk_terminal.due_diligence import finviz_view as finviz" ] }, { 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") diff --git a/terminal.py b/terminal.py index 56783314efd..5223d568dae 100644 --- a/terminal.py +++ b/terminal.py @@ -15,7 +15,7 @@ from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal import thought_of_the_day as thought from gamestonk_terminal import res_menu as rm from gamestonk_terminal.discovery import disc_controller -from gamestonk_terminal.due_diligence import dd_menu as ddm +from gamestonk_terminal.due_diligence import dd_controller from gamestonk_terminal.fundamental_analysis import fa_menu as fam from gamestonk_terminal.helper_funcs import b_is_stock_market_open, get_flair from gamestonk_terminal.main_helper import clear, export, load, print_help, view, candle @@ -32,7 +32,7 @@ from gamestonk_terminal.portfolio import port_controller from gamestonk_terminal.cryptocurrency import crypto_controller from gamestonk_terminal.screener import screener_controller from gamestonk_terminal.oanda import oanda_controller - +from gamestonk_terminal.portfolio_optimization import po_controller # import warnings # warnings.simplefilter("always") @@ -91,7 +91,9 @@ def main(): "crypto", "ra", "fx", + "po", ] + menu_parser.add_argument("opt", choices=choices) completer = NestedCompleter.from_nested_dict({c: None for c in choices}) @@ -180,7 +182,8 @@ def main(): else: print( - "No ticker selected. Use 'load ticker' to load the ticker you want to look at." + "No ticker selected. Use 'load ticker' to load the ticker you want to look at.", + "\n", ) main_cmd = True @@ -213,7 +216,7 @@ def main(): b_quit = ta_controller.menu(df_stock, s_ticker, s_start, s_interval) elif ns_known_args.opt == "dd": - b_quit = ddm.dd_menu(df_stock, s_ticker, s_start, s_interval) + b_quit = dd_controller.menu(df_stock, s_ticker, s_start, s_interval) elif ns_known_args.opt == "eda": if s_interval == "1440min": @@ -236,7 +239,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() @@ -247,6 +252,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: @@ -257,7 +265,7 @@ def main(): try: # pylint: disable=import-outside-toplevel - from gamestonk_terminal.prediction_techniques import pred_menu as pm + from gamestonk_terminal.prediction_techniques import pred_controller except ModuleNotFoundError as e: print("One of the optional packages seems to be missing") print("Optional packages need to be installed") @@ -270,7 +278,7 @@ def main(): continue if s_interval == "1440min": - b_quit = pm.pred_menu(df_stock, s_ticker, s_start, s_interval) + b_quit = pred_controller.menu(df_stock, 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 @@ -287,7 +295,7 @@ def main(): # pylint: disable=no-member df_stock_pred = df_stock_pred.sort_index(ascending=True) df_stock_pred = df_stock_pred[s_start:] - b_quit = pm.pred_menu( + b_quit = pred_controller.menu( df_stock_pred, s_ticker, s_start, s_interval="1440min" ) except Exception as e: diff --git a/tests/test_dd_financial_modeling_prep_api.py b/tests/test_dd_financial_modeling_prep_api.py deleted file mode 100644 index 952fecdfb73..00000000000 --- a/tests/test_dd_financial_modeling_prep_api.py +++ /dev/null @@ -1,9 +0,0 @@ -""" fundamental_analysis/business_insider_api.py tests """ -import unittest - -from gamestonk_terminal.due_diligence.financial_modeling_prep_api import rating - - -class TestDdFinancialModelingPrepApi(unittest.TestCase): - def test_rating(self): - rating([], "PLTR") diff --git a/tests/test_dd_financial_modeling_prep_view.py b/tests/test_dd_financial_modeling_prep_view.py new file mode 100644 index 00000000000..0db0c299699 --- /dev/null +++ b/tests/test_dd_financial_modeling_prep_view.py @@ -0,0 +1,9 @@ +""" fundamental_analysis/business_insider_api.py tests """ +import unittest + +from gamestonk_terminal.due_diligence.financial_modeling_prep_view import rating + + +class TestDdFinancialModelingPrepView(unittest.TestCase): + def test_rating(self): + rating([], "PLTR") diff --git a/tests/test_dd_finviz_api.py b/tests/test_dd_finviz_view.py similarity index 70% rename from tests/test_dd_finviz_api.py rename to tests/test_dd_finviz_view.py index b0e1e680b2e..8c062510029 100644 --- a/tests/test_dd_finviz_api.py +++ b/tests/test_dd_finviz_view.py @@ -1,7 +1,7 @@ """ due_diligence/finviz_api.py tests """ import unittest -from gamestonk_terminal.due_diligence.finviz_api import analyst +from gamestonk_terminal.due_diligence.finviz_view import analyst class TestDdFinvizApi(unittest.TestCase):