diff --git a/README.md b/README.md index 54c5089fe7f..7724ad3300c 100644 --- a/README.md +++ b/README.md @@ -257,10 +257,14 @@ These are the ones where a key is necessary: * Financial Modeling Prep: https://financialmodelingprep.com/developer * FRED: https://fred.stlouisfed.org/docs/api/api_key.html * News API: https://newsapi.org + * Tradier: https://developer.tradier.com/getting_started When these are obtained, don't forget to update [config_terminal.py](/gamestonk_terminal/config_terminal.py). -Alternatively, you can also set them to the following environment variables: GT_API_KEY_ALPHAVANTAGE, GT_API_KEY_FINANCIALMODELINGPREP, GT_API_KEY_QUANDL, GT_API_REDDIT_CLIENT_ID, GT_API_REDDIT_CLIENT_SECRET, GT_API_REDDIT_USERNAME, GT_API_REDDIT_USER_AGENT, GT_API_REDDIT_PASSWORD, GT_API_TWITTER_KEY, GT_API_TWITTER_SECRET_KEY, GT_API_TWITTER_BEARER_TOKEN, GT_API_POLYGON_KEY, GT_FRED_API_KEY, GT_API_NEWS_TOKEN. +Alternatively, you can also set them to the following environment variables: GT_API_KEY_ALPHAVANTAGE, +GT_API_KEY_FINANCIALMODELINGPREP, GT_API_KEY_QUANDL, GT_API_REDDIT_CLIENT_ID, GT_API_REDDIT_CLIENT_SECRET, GT_API_REDDIT_USERNAME, +GT_API_REDDIT_USER_AGENT, GT_API_REDDIT_PASSWORD, GT_API_TWITTER_KEY, GT_API_TWITTER_SECRET_KEY, +GT_API_TWITTER_BEARER_TOKEN, GT_API_POLYGON_KEY, GT_FRED_API_KEY, GT_API_NEWS_TOKEN, GT_TRADIER_TOKEN . Example: ``` @@ -359,6 +363,9 @@ Distributed under the MIT License. See [LICENSE](https://github.com/DidierRLopes [Artem Veremy](https://www.linkedin.com/in/veremey/) - artem@veremey.net [James Maslek](https://www.linkedin.com/in/james-maslek-b6810186/) - jmaslek11@gmail.com + +Feel free to share loss porn, memes or any questions at https://discord.gg/FmKjHKMn + ## Acknowledgments * [VICE article](https://www.vice.com/en/article/qjp9vp/gamestonk-terminal-is-a-diy-meme-stock-version-of-bloomberg-terminal) diff --git a/ROADMAP.md b/ROADMAP.md index 1642af3fe10..277662bd9a8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -14,6 +14,7 @@ * [Exploratory Data Analysis](#Exploratory-Data-Analysis) * [Residual Analysis](#Residual-Analysis) * [Portfolio Analysis](#Portfolio-Analysis) +* [Portfolio Optimization](#Portfolio-Optimization) * [FRED](#FRED) * [Options](#Options) * [Credit Analysis](#Credit-Analysis) @@ -33,6 +34,8 @@ * [x] Top Losers (@didier) - [PR #171](https://github.com/DidierRLopes/GamestonkTerminal/pull/171) * [x] ARK orders (@aia) - [PR #140](https://github.com/DidierRLopes/GamestonkTerminal/pull/140) * [x] Analyse sectors/industry performance, overview and valuation from Finviz (@didier) - [PR #315](https://github.com/DidierRLopes/GamestonkTerminal/pull/315) +* [x] Add latest and trending news commands (@hinxx) - [PR #347](https://github.com/DidierRLopes/GamestonkTerminal/pull/347) +* [x] Add top ratings updates (@hinxx) - [PR #357](https://github.com/DidierRLopes/GamestonkTerminal/pull/357) **NEXT** @@ -76,6 +79,8 @@ --- ## Due Diligence +* [x] Add dark pools (ATS) vs Non-ATS data over time (@didier) - [PR #363](https://github.com/DidierRLopes/GamestonkTerminal/pull/363) +* [x] Add failure to deliver command (@hinxx) - [PR #366](#https://github.com/DidierRLopes/GamestonkTerminal/pull/366) **NEXT** @@ -110,6 +115,7 @@ * [x] Add multi-ticker sentiment comparison (@didier) - [PR #250](https://github.com/DidierRLopes/GamestonkTerminal/pull/250) * [x] Get similar companies from Finviz based on Industry and Sector (and possibly country) from Finviz (@didier) - [PR #323](https://github.com/DidierRLopes/GamestonkTerminal/pull/323) * [x] Output brief screen (overview, performance, financial, valuation) across similar companies (@didier) - [PR #323](https://github.com/DidierRLopes/GamestonkTerminal/pull/323) +* [x] Add command to take tickers to portfolio optimization (@jmaslek) - [PR #329](https://github.com/DidierRLopes/GamestonkTerminal/pull/329) **NEXT** @@ -142,7 +148,16 @@ * [ ] 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) +* [x] Add command to maximise the quadratic utility(@didier) - [PR #349](https://github.com/DidierRLopes/GamestonkTerminal/pull/349) + +**NEXT** +* [ ] Allow for more custom optimization constrains + --- @@ -184,6 +199,7 @@ * [x] Add README with explanation of how presets are stored and can be added by experienced users. (@didier) - [PR #314](https://github.com/DidierRLopes/GamestonkTerminal/pull/314) * [x] Add screener signals (e.g. top gainers, new highs, most volatile, oversold, major news, ...) from Finviz (@didier) - [PR #314](https://github.com/DidierRLopes/GamestonkTerminal/pull/314) * [x] Plot screener historical using Yahoo Finance data (@didier) - [PR #319](https://github.com/DidierRLopes/GamestonkTerminal/pull/319) +* [x] Add command to take tickers to portfolio optimization (@didier) - [PR #349](https://github.com/DidierRLopes/GamestonkTerminal/pull/349) **NEXT** diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 4ab6d9c5790..fdaa9118d84 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -9,6 +9,7 @@ * [Due Diligence](#Due-Diligence-) * [Prediction Techniques](#Prediction-Techniques-) * [Portfolio Analysis](#Portfolio-Analysis-) +* [Portfolio Optimization](#Portfolio-Optimization-) * [Cryptocurrencies](#Cryptocurrencies-) * [Comparison Analysis](#Comparison-Analysis-) * [Exploratory Data Analysis](#Exploratory-Data-Analysis-) @@ -79,6 +80,9 @@ Command|Description|Source `valuation` |valuation of sectors, industry, country |[Finviz](https://finviz.com) `performance` |performance of sectors, industry, country |[Finviz](https://finviz.com) `spectrum` |spectrum of sectors, industry, country |[Finviz](https://finviz.com) +`latest` |latest news |[Seeking Alpha](https://seekingalpha.com/) +`trending` |trending news |[Seeking Alpha](https://seekingalpha.com/) +`ratings ` |top ratings updates |[MarketBeat](https://marketbeat.com)   @@ -209,6 +213,8 @@ Command|Description|Source `sec` |SEC filings |[MarketWatch](https://www.marketwatch.com/) `short` |short interest |[Quandl](https://www.quandl.com/) `warnings` |company warnings according to Sean Seah book |[MarketWatch](https://www.marketwatch.com/) +`dp` |dark pools (ATS) vs OTC data [FINRA](https://www.finra.org/#/) +`ftd` |display fails-to-deliver data [SEC](https://www.sec.gov)   @@ -241,6 +247,24 @@ Command|Description|Brokers `hold` | view net holdings across all logins   +## Portfolio Optimization [»](portfolio_optimization/README.md) + +Command|Description +------|------ +`add`| add ticker to optimize +`select`| overwrite current tickers with new tickers +`equal`| equally weighted +`property`| weight according to selected info property (e.g. marketCap) +`maxsharpe`| optimizes for maximal Sharpe ratio (a.k.a the tangency portfolio) +`minvol`| optimizes for minimum volatility +`maxquadutil`| maximises the quadratic utility, given some risk aversion +`effret`| maximises return for a given target risk +`effrisk`| minimises risk for a given target return +`ef`| show the efficient frontier + +  + + ## Cryptocurrency [»](cryptocurrency/README.md) Command|Description @@ -316,7 +340,9 @@ Command|Description Command|Description|Source ------ | --------|---- `exp` | see/set expiry date -`volume` | plot options trading volume / open interest |[Yahoo Finance](https://finance.yahoo.com/) +`volume` | volume + open interest options trading plot |[Yahoo Finance](https://finance.yahoo.com/) +`vcalls` | calls volume + open interest plot |[Yahoo Finance](https://finance.yahoo.com/) +`vputs` | puts volume + open interest plot |[Yahoo Finance](https://finance.yahoo.com/)   @@ -336,4 +362,4 @@ technical |technical (e.g. Beta, SMA50, 52W Low, RSI, Change) signals |view filter signals (e.g. -s top_gainers) -  \ No newline at end of file +  diff --git a/gamestonk_terminal/comparison_analysis/README.md b/gamestonk_terminal/comparison_analysis/README.md index f00b902183d..c60dfd305d0 100644 --- a/gamestonk_terminal/comparison_analysis/README.md +++ b/gamestonk_terminal/comparison_analysis/README.md @@ -32,6 +32,8 @@ This menu aims to compare a pre-loaded stock with similar companies, and the usa * brief performance (e.g. Perf Week, Perf YTD, Volatility M) [Finviz] * [technical](#technical) * brief technical (e.g. Beta, SMA50, 52W Low, RSI, Change) [Finviz] +* [> po](portfolio_optimization/README.md) + * portfolio optimization for selected tickers ## get diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py index 3214c4c6b76..499b6c9cd39 100644 --- a/gamestonk_terminal/comparison_analysis/ca_controller.py +++ b/gamestonk_terminal/comparison_analysis/ca_controller.py @@ -16,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 @@ -43,6 +44,7 @@ class ComparisonAnalysisController: "ownership", "performance", "technical", + "po", ] def __init__( @@ -82,8 +84,6 @@ class ComparisonAnalysisController: if self.similar: print(f"[{self.user}] Similar Companies: {', '.join(self.similar)}") - # else: - # print(f"No similar companies [{self.user}]") print("\nComparison Analysis Mode:") print(" help show this comparison analysis menu again") @@ -108,6 +108,9 @@ class ComparisonAnalysisController: print(" performance brief performance comparison") print(" technical brief technical comparison") print("") + if self.similar: + print(" > po portfolio optimization for selected tickers") + print("") return @staticmethod @@ -315,6 +318,10 @@ class ComparisonAnalysisController: """Process technical command""" finviz_compare_view.screener(other_args, "technical", self.ticker, self.similar) + def call_po(self, _): + """Call the portfolio optimization menu with selected tickers""" + 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/config_terminal.py b/gamestonk_terminal/config_terminal.py index 4189a27ce4e..daf66a26be6 100644 --- a/gamestonk_terminal/config_terminal.py +++ b/gamestonk_terminal/config_terminal.py @@ -35,3 +35,6 @@ API_NEWS_TOKEN = os.getenv("GT_API_NEWS_TOKEN") or "REPLACE_ME" # Robinhood RH_USERNAME = os.getenv("GT_RH_USERNAME") or "REPLACE_ME" RH_PASSWORD = os.getenv("GT_RH_PASSWORD") or "REPLACE_ME" + +# https://tradier.com/products/market-data-api +TRADIER_TOKEN = os.getenv("GT_TRADIER_TOKEN") or "REPLACE_ME" diff --git a/gamestonk_terminal/discovery/README.md b/gamestonk_terminal/discovery/README.md index 517d142d334..6b20d1b25f1 100644 --- a/gamestonk_terminal/discovery/README.md +++ b/gamestonk_terminal/discovery/README.md @@ -32,6 +32,12 @@ This menu aims to discover new stocks, and the usage of the following commands a * performance of sectors, industry, country [Finviz] * [spectrum](#spectrum) * spectrum of sectors, industry, country [Finviz] +* [latest](#latest) + * latest news [Seeking Alpha] +* [trending](#trending) + * trending news [Seeking Alpha] +* [ratings](#ratings) + * top ratings updates [MarketBeat] ## map @@ -218,3 +224,44 @@ Spectrum of sectors, industry, country. [Source: Finviz] * -g : Data group (sector, industry or country). Default: Sector. ![cntry_spec](https://user-images.githubusercontent.com/25267873/113639067-48fd5e80-9670-11eb-95cf-0931845ddd12.png) + + +## latest + +``` +usage: latest [-i N_ID] [-n N_NUM] [-d DATE] +``` + +Latest news articles. [Source: Seeking Alpha] +* -i : Article ID number. +* -n : Number of articles being printed. Default 10. +* -d : Date of news article. + +latest + + +## trending + +``` +usage: trending [-i N_ID] [-n N_NUM] +``` + +Trending news articles. [Source: Seeking Alpha] +* -i : Article ID number. +* -n : Number of articles being printed. Default 10. + +trending + + +## ratings + +``` +usage: ratings [-t N_THRESHOLD] +``` + +Top ratings updates. [Source: MarketBeat] + +* -t : Minimum threshold in percentage change between current and target price to show ratings. Default: 100. + +ratings + diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index 07bb33a3302..2362e8f762b 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -20,6 +20,7 @@ from gamestonk_terminal.discovery import ( spachero_view, unusual_whales_view, yahoo_finance_view, + marketbeat_view, ) @@ -46,6 +47,9 @@ class DiscoveryController: "valuation", "performance", "spectrum", + "ratings", + "latest", + "trending", ] def __init__(self): @@ -87,6 +91,9 @@ class DiscoveryController: print(" valuation valuation of sectors, industry, country [Finviz]") print(" performance performance of sectors, industry, country [Finviz]") print(" spectrum spectrum of sectors, industry, country [Finviz]") + print(" latest latest news [Seeking Alpha]") + print(" trending trending news [Seeking Alpha]") + print(" ratings top ratings updates [MarketBeat]") print("") def switch(self, an_input: str): @@ -187,6 +194,18 @@ class DiscoveryController: other_args, "spectrum" ) + def call_latest(self, other_args: List[str]): + """Process latest command""" + seeking_alpha_view.latest_news_view(other_args) + + def call_trending(self, other_args: List[str]): + """Process trending command""" + seeking_alpha_view.trending_news_view(other_args) + + def call_ratings(self, other_args: List[str]): + """Process ratings command""" + marketbeat_view.ratings_view(other_args) + def menu(): """Discovery Menu""" diff --git a/gamestonk_terminal/discovery/marketbeat_model.py b/gamestonk_terminal/discovery/marketbeat_model.py new file mode 100755 index 00000000000..b8bd10241e4 --- /dev/null +++ b/gamestonk_terminal/discovery/marketbeat_model.py @@ -0,0 +1,93 @@ +""" MarketBeat Model """ +__docformat__ = "numpy" + +from typing import List +import requests +from bs4 import BeautifulSoup + +from gamestonk_terminal.helper_funcs import get_user_agent + + +def get_ratings_html(url_ratings: str) -> str: + """Wraps HTTP requests.get for testibility + + Parameters + ---------- + url_ratings : str + Ratings URL + + Returns + ------- + str + HTML page of ratings + """ + ratings_html = requests.get( + url_ratings, headers={"User-Agent": get_user_agent()} + ).text + + return ratings_html + + +def get_ratings() -> List[dict]: + """Returns a list of ratings + + Parameters + ---------- + None + + Returns + ------- + list + Ratings list + """ + + ratings = list() + url_ratings = "https://www.marketbeat.com/ratings/" + + text_soup_ratings = BeautifulSoup( + get_ratings_html(url_ratings), + "lxml", + ) + + for stock_rows in text_soup_ratings.findAll("tr"): + tds = stock_rows.findAll("td") + if len(tds) == 8: + rating = {} + # company + td = tds[0] + company = td.find("div", {"class": "title-area"}).text + ticker = td.find("div", {"class": "ticker-area"}).text + rating["company"] = company + rating["ticker"] = ticker + # action + td = tds[1] + action = td.text + rating["action"] = action + # brokerage + td = tds[2] + brokerage = td.find("a").text + rating["brokerage"] = brokerage + # current price + td = tds[3] + if td.find("span"): + td.span.extract() + current_price = td.text + rating["current_price"] = current_price + # target price + td = tds[4] + target_price = td.text + rating["target_price"] = target_price + # new rating + td = tds[5] + new_rating = td.text + rating["rating"] = new_rating + # impact on price + td = tds[6] + if td.find("a"): + impact = td.find("a").text + else: + impact = td.text + rating["impact"] = impact + ratings.append(rating) + + return ratings diff --git a/gamestonk_terminal/discovery/marketbeat_view.py b/gamestonk_terminal/discovery/marketbeat_view.py new file mode 100644 index 00000000000..13ec048ee5c --- /dev/null +++ b/gamestonk_terminal/discovery/marketbeat_view.py @@ -0,0 +1,116 @@ +""" MarketBeat View """ +__docformat__ = "numpy" + +import argparse +import numpy as np +import pandas as pd +from typing import List +from gamestonk_terminal.helper_funcs import ( + check_positive, + parse_known_args_and_warn, +) + +from gamestonk_terminal.discovery import marketbeat_model + + +def ratings_view(other_args: List[str]): + """Prints top ratings updates + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-t", "100"] + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="ratings", + description="""Top ratings updates. [Source: MarketBeat]""", + ) + parser.add_argument( + "-t", + "--threshold", + action="store", + dest="n_threshold", + type=check_positive, + default=100, + help="Minimum threshold in percentage change between current and target price to show ratings", + ) + + 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 + + try: + ratings = marketbeat_model.get_ratings() + + df_ratings = pd.DataFrame(ratings) + df_ratings = df_ratings[df_ratings["target_price"] != ""] + df_ratings["old_target"] = df_ratings["target_price"].apply( + lambda x: x.split(" ➝ ")[0] if "➝" in x else "" + ) + df_ratings["new_target"] = df_ratings["target_price"].apply( + lambda x: x.split(" ➝ ")[1] if "➝" in x else x + ) + + df_ratings["clean_current_price"] = df_ratings["current_price"].apply( + lambda x: "".join([c if c.isdigit() or c == "." else "" for c in str(x)]) + ) + df_ratings["clean_current_price"] = df_ratings["clean_current_price"].astype( + float + ) + df_ratings["clean_target_price"] = df_ratings["new_target"].apply( + lambda x: "".join([c if c.isdigit() or c == "." else "" for c in str(x)]) + ) + df_ratings["clean_target_price"] = df_ratings["clean_target_price"].astype( + float + ) + + df_ratings["pct_increase"] = ( + 100 + * (df_ratings["clean_target_price"] - df_ratings["clean_current_price"]) + / df_ratings["clean_current_price"] + ) + df_ratings["pct_increase"] = df_ratings["pct_increase"].round(2) + + df_ratings["pct_abs"] = np.abs(df_ratings["pct_increase"]) + df_ratings["pct_abs"] = df_ratings["pct_abs"].round(2) + + df_ratings = df_ratings.sort_values("pct_abs", ascending=False) + + df_ratings_top = df_ratings[df_ratings["pct_abs"] > ns_parser.n_threshold][ + [ + "ticker", + "brokerage", + "rating", + "current_price", + "old_target", + "new_target", + "pct_increase", + ] + ] + + df_ratings_top["pct_increase"] = df_ratings_top["pct_increase"].apply( + lambda x: str(x) + " %" + ) + + print( + df_ratings_top.rename( + columns={ + "ticker": "Ticker", + "brokerage": "Brokerage", + "rating": "Rating", + "current_price": "Current Price", + "old_target": "Old Target", + "new_target": "New Target", + "pct_increase": "Percentage Increase", + } + ).to_string(index=False) + ) + + print("") + except Exception as e: + print(e, "\n") diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index 59f63e44ae6..6041eb63f67 100755 --- a/gamestonk_terminal/discovery/seeking_alpha_model.py +++ b/gamestonk_terminal/discovery/seeking_alpha_model.py @@ -1,10 +1,12 @@ """ Seeking Alpha Model """ __docformat__ = "numpy" +from typing import List import requests from bs4 import BeautifulSoup import pandas as pd from pandas.core.frame import DataFrame +from datetime import datetime from gamestonk_terminal.helper_funcs import get_user_agent @@ -67,3 +69,145 @@ def get_next_earnings(pages: int) -> DataFrame: df_earnings = df_earnings.set_index("Date") return df_earnings + + +def get_articles_html(url_articles: str) -> str: + """Wraps HTTP requests.get for testibility + + Parameters + ---------- + url_articles : str + Articles URL + + Returns + ------- + str + HTML page of articles + """ + articles_html = requests.get( + url_articles, headers={"User-Agent": get_user_agent()} + ).text + + return articles_html + + +def get_article_list(start_date: datetime, num: int) -> List[dict]: + """Returns a list of latest articles + + Parameters + ---------- + start_date : datetime + Starting date + num : int + Number of articles + + Returns + ------- + list + Latest articles list + """ + + articles: List[dict] = [] + page = 1 + url_articles = f"https://seekingalpha.com/market-news/{page}" + while len(articles) < num: + text_soup_articles = BeautifulSoup( + get_articles_html(url_articles), + "lxml", + ) + + for item_row in text_soup_articles.findAll("li", {"class": "item"}): + item = item_row.find("a", {"class": "add-source-assigned"}) + if item is None: + continue + article_url = item["href"] + if not article_url.startswith("/news/"): + continue + article_id = article_url.split("/")[2].split("-")[0] + article_date = datetime.strptime( + item_row["data-last-date"], "%Y-%m-%d %H:%M:%S %z" + ) + if start_date.date() < article_date.date(): + continue + articles.append( + { + "title": item.text, + "publishedAt": article_date.strftime("%Y-%m-%d %H:%M:%S"), + "url": "https://seekingalpha.com" + article_url, + "id": article_id, + } + ) + + page += 1 + url_articles = f"https://seekingalpha.com/market-news/{page}" + + return articles + + +def get_trending_list(num: int) -> list: + """Returns a list of trending articles + + Parameters + ---------- + pages : int + Number of articles + + Returns + ------- + list + Trending articles list + """ + + articles = list() + url_articles = "https://seekingalpha.com/news/trending_news" + response = requests.get(url_articles, headers={"User-Agent": get_user_agent()}) + + # Check that the API response was successful + if response.status_code != 200: + print("Invalid response\n") + else: + for item in response.json(): + article_url = item["uri"] + if not article_url.startswith("/news/"): + continue + article_id = article_url.split("/")[2].split("-")[0] + articles.append( + { + "title": item["title"], + "publishedAt": item["publish_on"][: item["publish_on"].rfind(".")], + "url": "https://seekingalpha.com" + article_url, + "id": article_id, + } + ) + + return articles + + +def get_article_data(article_id: int) -> dict: + """Returns an article + + Parameters + ---------- + article_id : int + Article ID + + Returns + ------- + dict + Article data + """ + + article_url = f"https://seekingalpha.com/api/v3/news/{article_id}" + response = requests.get(article_url, headers={"User-Agent": get_user_agent()}) + jdata = response.json() + content = jdata["data"]["attributes"]["content"].replace("", "\n") + content = BeautifulSoup(content, features="html.parser").get_text() + + article = { + "title": jdata["data"]["attributes"]["title"], + "publishedAt": jdata["data"]["attributes"]["lastModified"], + "url": "https://seekingalpha.com" + jdata["data"]["links"]["self"], + "content": content, + } + + return article diff --git a/gamestonk_terminal/discovery/seeking_alpha_view.py b/gamestonk_terminal/discovery/seeking_alpha_view.py index 2965bf79cce..1670cc3e084 100644 --- a/gamestonk_terminal/discovery/seeking_alpha_view.py +++ b/gamestonk_terminal/discovery/seeking_alpha_view.py @@ -4,9 +4,11 @@ __docformat__ = "numpy" import argparse from typing import List import pandas as pd +from datetime import datetime from gamestonk_terminal.helper_funcs import ( check_positive, parse_known_args_and_warn, + valid_date, ) from gamestonk_terminal.discovery import seeking_alpha_model @@ -65,3 +67,158 @@ def earnings_release_dates_view(other_args: List[str]): ].to_string(index=False, header=False) ) print("") + + +def latest_news_view(other_args: List[str]): + """Prints the latest news article list + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-i", "123123", "-n", "5"] + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="latest", + description="""Latest news articles. [Source: Seeking Alpha]""", + ) + parser.add_argument( + "-i", + "--id", + action="store", + dest="n_id", + type=check_positive, + default=-1, + help="article ID number", + ) + parser.add_argument( + "-n", + "--num", + action="store", + dest="n_num", + type=check_positive, + default=10, + help="number of articles being printed", + ) + parser.add_argument( + "-d", + "--date", + action="store", + dest="n_date", + type=valid_date, + default=datetime.now().strftime("%Y-%m-%d"), + help="starting date", + ) + + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-i") + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + # User wants to see all latest news + if ns_parser.n_id == -1: + articles = seeking_alpha_model.get_article_list( + ns_parser.n_date, ns_parser.n_num + ) + for idx, article in enumerate(articles): + print( + article["publishedAt"].replace("T", " ").replace("Z", ""), + "-", + article["id"], + "-", + article["title"], + ) + print(article["url"]) + print("") + + if idx >= ns_parser.n_num - 1: + break + + # User wants to access specific article + else: + article = seeking_alpha_model.get_article_data(ns_parser.n_id) + print( + article["publishedAt"][: article["publishedAt"].rfind(":") - 3].replace( + "T", " " + ), + " ", + article["title"], + ) + print(article["url"]) + print("") + print(article["content"]) + + +def trending_news_view(other_args: List[str]): + """Prints the trending news article list + + Parameters + ---------- + other_args : List[str] + argparse other args - ["i", "123123", "-n", "5"] + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="trending", + description="""Trending news articles. [Source: Seeking Alpha]""", + ) + parser.add_argument( + "-i", + "--id", + action="store", + dest="n_id", + type=check_positive, + default=-1, + help="article ID number", + ) + parser.add_argument( + "-n", + "--num", + action="store", + dest="n_num", + type=check_positive, + default=10, + help="number of articles being printed", + ) + + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-i") + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + # User wants to see all trending articles + if ns_parser.n_id == -1: + articles = seeking_alpha_model.get_trending_list(ns_parser.n_num) + for idx, article in enumerate(articles): + print( + article["publishedAt"].replace("T", " ").replace("Z", ""), + "-", + article["id"], + "-", + article["title"], + ) + print(article["url"]) + print("") + + if idx >= ns_parser.n_num - 1: + break + + # User wants to access specific article + else: + article = seeking_alpha_model.get_article_data(ns_parser.n_id) + print( + article["publishedAt"][: article["publishedAt"].rfind(":") - 3].replace( + "T", " " + ), + " ", + article["title"], + ) + print(article["url"]) + print("") + print(article["content"]) diff --git a/gamestonk_terminal/due_diligence/README.md b/gamestonk_terminal/due_diligence/README.md index 70bfb49340b..cb8854b78e5 100644 --- a/gamestonk_terminal/due_diligence/README.md +++ b/gamestonk_terminal/due_diligence/README.md @@ -24,6 +24,10 @@ This menu aims to help in due-diligence of a pre-loaded stock, and the usage of * short interest [Quandl] * [warnings](#warnings) * company warnings according to Sean Seah book [Market Watch] +* [dp](#dp) + * dark pools (ATS) vs OTC data [FINRA] +* [ftd](#ftd) + * fails-to-deliver data [SEC] ## news @@ -157,3 +161,26 @@ Sean Seah warnings. Check: Consistent historical earnings per share; Consistentl * -d : print insights into warnings calculation. Default False. warnings + +## dp + +```text +usage: dp +``` + +Display barchart of dark pool (ATS) and OTC (Non ATS) data + +![dp](https://user-images.githubusercontent.com/25267873/115130908-7987b580-9feb-11eb-8bca-1999174178d0.png) + +## ftd + +```text +usage: ftd [-n N_NUM] +``` + +The fails-to-deliver data collected by SEC. Fails to deliver on a given day are a cumulative number of all fails outstanding until that day, plus new fails that occur that day, less fails that settle that day. See . [Source: SEC] + +* -n : number of latest fails-to-deliver being printed. Default 20. + +![ftd](https://user-images.githubusercontent.com/25267873/115201750-93a9bc80-a0ed-11eb-8f13-68508749259e.png) + diff --git a/gamestonk_terminal/due_diligence/dd_controller.py b/gamestonk_terminal/due_diligence/dd_controller.py index 14ecab8fc40..2dbe1379927 100644 --- a/gamestonk_terminal/due_diligence/dd_controller.py +++ b/gamestonk_terminal/due_diligence/dd_controller.py @@ -13,6 +13,8 @@ from gamestonk_terminal.due_diligence import market_watch_view as mw_view from gamestonk_terminal.due_diligence import quandl_view as q_view from gamestonk_terminal.due_diligence import reddit_view as r_view from gamestonk_terminal.due_diligence import news_view +from gamestonk_terminal.due_diligence import finra_view +from gamestonk_terminal.due_diligence import sec_view from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import get_flair from gamestonk_terminal.menu import session @@ -37,6 +39,8 @@ class DueDiligenceController: "analyst", "warnings", "sec", + "dp", + "ftd", ] def __init__(self, stock: DataFrame, ticker: str, start: str, interval: str): @@ -97,6 +101,8 @@ class DueDiligenceController: print( " warnings company warnings according to Sean Seah book [Market Watch]" ) + print(" dp dark pools (ATS) vs OTC data [FINRA]") + print(" ftd fails-to-deliver data [SEC]") print("") def switch(self, an_input: str): @@ -175,6 +181,14 @@ class DueDiligenceController: """ Process short command """ q_view.short_interest(other_args, self.ticker, self.start) + def call_dp(self, other_args: List[str]): + """ Process dp command """ + finra_view.dark_pool(other_args, self.ticker) + + def call_ftd(self, other_args: List[str]): + """ Process ftd command """ + sec_view.fails_to_deliver(other_args, self.ticker) + def menu(stock: DataFrame, ticker: str, start: str, interval: str): """Due Diligence Menu diff --git a/gamestonk_terminal/due_diligence/finra_view.py b/gamestonk_terminal/due_diligence/finra_view.py new file mode 100644 index 00000000000..ab4fc2465aa --- /dev/null +++ b/gamestonk_terminal/due_diligence/finra_view.py @@ -0,0 +1,307 @@ +""" FINRA View """ +__docformat__ = "numpy" + +import argparse +from typing import List, Tuple +import requests +import pandas as pd +from matplotlib import pyplot as plt +from gamestonk_terminal.config_plot import PLOT_DPI +from gamestonk_terminal import feature_flags as gtff +from gamestonk_terminal.helper_funcs import parse_known_args_and_warn, plot_autoscale + + +def getFINRAweeks(tier, is_ats) -> List: + """Get FINRA weeks + + Parameters + ---------- + tier : str + Stock tier between T1, T2, or OTCE + is_ats : bool + ATS data if true, NON-ATS otherwise + + Returns + ---------- + List + List of response data + """ + req_hdr = {"Accept": "application/json", "Content-Type": "application/json"} + + req_data = { + "compareFilters": [ + { + "compareType": "EQUAL", + "fieldName": "summaryTypeCode", + "fieldValue": "ATS_W_SMBL" if is_ats else "OTC_W_SMBL", + }, + { + "compareType": "EQUAL", + "fieldName": "tierIdentifier", + "fieldValue": tier, + }, + ], + "delimiter": "|", + "fields": ["weekStartDate"], + "limit": 27, + "quoteValues": False, + "sortFields": ["-weekStartDate"], + } + + response = requests.post( + "https://api.finra.org/data/group/otcMarket/name/weeklyDownloadDetails", + headers=req_hdr, + json=req_data, + ) + + return response.json() if response.status_code == 200 else list() + + +def getFINRAdata(weekStartDate, tier, ticker, is_ats) -> Tuple[int, List]: + """Get FINRA data + + Parameters + ---------- + weekStartDate : str + Weekly data to get FINRA data + tier : str + Stock tier between T1, T2, or OTCE + ticker : str + Stock ticker to get data from + is_ats : bool + ATS data if true, NON-ATS otherwise + + Returns + ---------- + str + Status code from request + List + List of response data + """ + req_hdr = {"Accept": "application/json", "Content-Type": "application/json"} + + l_cmp_filters = [ + { + "compareType": "EQUAL", + "fieldName": "weekStartDate", + "fieldValue": weekStartDate, + }, + {"compareType": "EQUAL", "fieldName": "tierIdentifier", "fieldValue": tier}, + { + "compareType": "EQUAL", + "description": "", + "fieldName": "summaryTypeCode", + "fieldValue": "ATS_W_SMBL" if is_ats else "OTC_W_SMBL", + }, + ] + + if ticker: + l_cmp_filters.append( + { + "compareType": "EQUAL", + "fieldName": "issueSymbolIdentifier", + "fieldValue": ticker, + } + ) + + req_data = { + "compareFilters": l_cmp_filters, + "delimiter": "|", + "fields": [ + "issueSymbolIdentifier", + "totalWeeklyShareQuantity", + "totalWeeklyTradeCount", + "lastUpdateDate", + ], + "limit": 5000, + "quoteValues": False, + "sortFields": ["totalWeeklyShareQuantity"], + } + + response = requests.post( + "https://api.finra.org/data/group/otcMarket/name/weeklySummary", + headers=req_hdr, + json=req_data, + ) + + return ( + response.status_code, + response.json() if response.status_code == 200 else list(), + ) + + +def getTickerFINRAdata(ticker) -> Tuple[pd.DataFrame, pd.DataFrame]: + """Get all FINRA data associated with a ticker + + Parameters + ---------- + ticker : str + Stock ticker to get data from + + Returns + ---------- + pd.DataFrame + Dark Pools (ATS) Data + pd.DataFrame + OTC (Non-ATS) Data + """ + tiers = ["T1", "T2", "OTCE"] + + l_data = list() + for tier in tiers: + for d_week in getFINRAweeks(tier, is_ats=True): + status_code, response = getFINRAdata( + d_week["weekStartDate"], tier, ticker, True + ) + if status_code == 200: + if response: + d_data = response[0] + d_data.update(d_week) + l_data.append(d_data) + else: + break + + df_ats = pd.DataFrame(l_data) + if not df_ats.empty: + df_ats = df_ats.sort_values("weekStartDate") + df_ats = df_ats.set_index("weekStartDate") + + l_data = list() + for tier in tiers: + for d_week in getFINRAweeks(tier, is_ats=False): + status_code, response = getFINRAdata( + d_week["weekStartDate"], tier, ticker, False + ) + if status_code == 200: + if response: + d_data = response[0] + d_data.update(d_week) + l_data.append(d_data) + else: + break + + df_otc = pd.DataFrame(l_data) + if not df_otc.empty: + df_otc = df_otc.sort_values("weekStartDate") + df_otc = df_otc.set_index("weekStartDate") + + return df_ats, df_otc + + +def plot_dark_pools(ticker: str, ats: pd.DataFrame, otc: pd.DataFrame): + """Plots ATS and NON-ATS data + + Parameters + ---------- + ticker : str + Stock ticker to get data from + ats : pd.DataFrame + Dark Pools (ATS) Data + otc : pd.DataFrame + OTC (Non-ATS) Data + """ + _, axData = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI) + + plt.subplot(3, 1, (1, 2)) + if not ats.empty and not otc.empty: + plt.bar( + ats.index, + (ats["totalWeeklyShareQuantity"] + otc["totalWeeklyShareQuantity"]) + / 1_000_000, + color="tab:orange", + ) + plt.bar( + otc.index, otc["totalWeeklyShareQuantity"] / 1_000_000, color="tab:blue" + ) + plt.legend(["ATS", "OTC"]) + + elif not ats.empty: + plt.bar( + ats.index, + ats["totalWeeklyShareQuantity"] / 1_000_000, + color="tab:orange", + ) + plt.legend(["ATS"]) + + elif not otc.empty: + plt.bar( + otc.index, otc["totalWeeklyShareQuantity"] / 1_000_000, color="tab:blue" + ) + plt.legend(["OTC"]) + + plt.ylabel("Total Weekly Shares [Million]") + plt.grid(b=True, which="major", color="#666666", linestyle="-", alpha=0.2) + plt.title(f"Dark Pools (ATS) vs OTC (Non-ATS) Data for {ticker}") + + plt.subplot(313) + if not ats.empty: + plt.plot( + ats.index, + ats["totalWeeklyShareQuantity"] / ats["totalWeeklyTradeCount"], + color="tab:orange", + ) + plt.legend(["ATS"]) + + if not otc.empty: + plt.plot( + otc.index, + otc["totalWeeklyShareQuantity"] / otc["totalWeeklyTradeCount"], + color="tab:blue", + ) + plt.legend(["ATS", "OTC"]) + + else: + plt.plot( + otc.index, + otc["totalWeeklyShareQuantity"] / otc["totalWeeklyTradeCount"], + color="tab:blue", + ) + plt.legend(["OTC"]) + + plt.ylabel("Shares per Trade") + plt.grid(b=True, which="major", color="#666666", linestyle="-", alpha=0.2) + plt.gcf().autofmt_xdate() + plt.xlabel("Weeks") + + if gtff.USE_ION: + plt.ion() + + plt.show() + + +def dark_pool(other_args: List[str], ticker: str): + """Display barchart of dark pool (ATS) and OTC (Non ATS) data + + Parameters + ---------- + other_args : List[str] + argparse other args + ticker : str + Stock ticker + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="dp", + description="Display barchart of dark pool (ATS) and OTC (Non ATS) data [Source: FINRA]", + ) + + if 1: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + df_ats, df_otc = getTickerFINRAdata(ticker) + + if df_ats.empty and df_otc.empty: + print("No ticker data found!") + + plot_dark_pools(ticker, df_ats, df_otc) + + print("") + + """ + except Exception as e: + print(e) + print("") + return + """ diff --git a/gamestonk_terminal/due_diligence/sec_view.py b/gamestonk_terminal/due_diligence/sec_view.py new file mode 100644 index 00000000000..9eb3f02e8f8 --- /dev/null +++ b/gamestonk_terminal/due_diligence/sec_view.py @@ -0,0 +1,108 @@ +""" SEC View """ +__docformat__ = "numpy" + +import argparse +from typing import List +from datetime import datetime +import requests +import pandas as pd +from bs4 import BeautifulSoup +from matplotlib import pyplot as plt +import matplotlib.dates as mdates +from gamestonk_terminal import feature_flags as gtff +from gamestonk_terminal.helper_funcs import ( + get_user_agent, + check_positive, + parse_known_args_and_warn, +) + + +def fails_to_deliver(other_args: List[str], ticker: str): + """Display fails-to-deliver data for a given ticker + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-n", "10"] + ticker : str + Stock ticker + """ + + parser = argparse.ArgumentParser( + add_help=False, + prog="ftd", + description="""Prints latest fails-to-deliver data. [Source: SEC]""", + ) + parser.add_argument( + "-n", + "--num", + action="store", + dest="n_num", + type=check_positive, + default=20, + help="number of latest fails-to-deliver being printed", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + url_ftds = "https://www.sec.gov/data/foiadocsfailsdatahtm" + text_soup_ftds = BeautifulSoup( + requests.get(url_ftds, headers={"User-Agent": get_user_agent()}).text, + "lxml", + ) + + table = text_soup_ftds.find("table", {"class": "list"}) + links = table.findAll("a") + link_idx = 0 + ftds_data = pd.DataFrame() + while len(ftds_data) < ns_parser.n_num: + if link_idx > len(links): + break + link = links[link_idx] + url = "https://www.sec.gov" + link["href"] + all_ftds = pd.read_csv( + url, + compression="zip", + sep="|", + engine="python", + skipfooter=2, + usecols=[0, 2, 3, 5], + dtype={"QUANTITY (FAILS)": "int"}, + ) + tmp_ftds = all_ftds[all_ftds["SYMBOL"] == ticker] + del tmp_ftds["PRICE"] + del tmp_ftds["SYMBOL"] + # merge the data from this archive + ftds_data = pd.concat([ftds_data, tmp_ftds], ignore_index=True) + link_idx += 1 + + # clip away extra rows + ftds_data = ftds_data.sort_values("SETTLEMENT DATE")[-ns_parser.n_num :] + ftds_data["SETTLEMENT DATE"] = ftds_data["SETTLEMENT DATE"].apply( + lambda x: datetime.strptime(str(x), "%Y%m%d") + ) + + plt.bar( + ftds_data["SETTLEMENT DATE"], + ftds_data["QUANTITY (FAILS)"] / 1000, + ) + plt.ylabel("Shares [K]") + plt.title(f"Fails-to-deliver Data for {ticker}") + plt.grid(b=True, which="major", color="#666666", linestyle="-", alpha=0.2) + plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%Y/%m/%d")) + plt.gca().xaxis.set_major_locator(mdates.DayLocator(interval=7)) + plt.gcf().autofmt_xdate() + plt.xlabel("Days") + + if gtff.USE_ION: + plt.ion() + + plt.show() + + print("") + + except Exception as e: + print(e, "\n") diff --git a/gamestonk_terminal/helper_funcs.py b/gamestonk_terminal/helper_funcs.py index 11a3ffbe196..a2e00fd3d35 100644 --- a/gamestonk_terminal/helper_funcs.py +++ b/gamestonk_terminal/helper_funcs.py @@ -347,7 +347,7 @@ def patch_pandas_text_adjustment(): def parse_known_args_and_warn(parser, l_args): parser.add_argument( - "-h", "--help", action="store_true", dest="help", help="show this help message" + "-h", "--help", action="store_true", help="show this help message" ) (ns_parser, l_unknown_args) = parser.parse_known_args(l_args) diff --git a/gamestonk_terminal/main_helper.py b/gamestonk_terminal/main_helper.py index 81238c99223..40c8f6714ba 100644 --- a/gamestonk_terminal/main_helper.py +++ b/gamestonk_terminal/main_helper.py @@ -60,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( diff --git a/gamestonk_terminal/options/README.md b/gamestonk_terminal/options/README.md index a86dceb2b2c..4742badb005 100644 --- a/gamestonk_terminal/options/README.md +++ b/gamestonk_terminal/options/README.md @@ -1,25 +1,89 @@ -# OPTIONS DATA +# OPTIONS -This menu aims to give insight on options. options can cause significant share price movement, and option pricing gives info about market sentiment for a ticker. +This menu aims to give insight on options. -* [volume](#volume) - * Show traded options for a given expiry [Yahoo Finance] +This menu aims to give insight on options. Options can cause significant share price movement, and option pricing gives info about market sentiment for a ticker. The usage of the following commands along with an example will be exploited below: + +* [exp](#exp) + * See/set expiry date [Yahoo Finance] +* [voi](#voi) + * Volume + open interest options trading plot [Yahoo Finance] +* [vcalls](#vcalls) + * Calls volume + open interest plot [Yahoo Finance] +* [vputs](#vputs) + * Puts volume + open interest plot [Yahoo Finance] +* [chains](#chains) + * Display option chains [Source: Tradier] - -## volume +## exp ```text -usage: volume -e yyyy-mm-dd +usage: exp [-d {0,1,2,3,4,5,6,7,8,9,10,11}] ``` -or +See/set expiry dates. [Source: Yahoo Finance] + +* -d : Expiry date index to set. + +exp + + +## voi ```text -usage: volume --expiry yyyy-mm-dd +usage: voi [-v MIN_VOL] [-m MIN_SP] [-M MAX_SP] ``` +Plots Volume + Open Interest of calls vs puts. [Source: Yahoo Finance] -Display volume graph for a date. [Source: Yahoo Finance] +* -v : Minimum volume (considering open interest) threshold of the plot. +* -m : Minimum strike price to consider in the plot. +* -M : Maximum strike price to consider in the plot. -* -e, --expiry : expiration date. +![voinio](https://user-images.githubusercontent.com/25267873/115161878-f7a19600-a097-11eb-889b-e9cc945f174d.png) + + +## vcalls + +```text +usage: vcalls [-m MIN_SP] [-M MAX_SP] +``` + +Plots Calls Volume + Open Interest. [Source: Yahoo Finance] + +* -m : Minimum strike price to consider in the plot. +* -M : Maximum strike price to consider in the plot. + +![vcalls](https://user-images.githubusercontent.com/25267873/115161874-f5d7d280-a097-11eb-93e2-3a2b20292f09.png) + + +## vputs + +```text +usage: vputs [-m MIN_SP] [-M MAX_SP] +``` + +Plots Puts Volume + Open Interest. [Source: Yahoo Finance] + +* -m : Minimum strike price to consider in the plot. +* -M : Maximum strike price to consider in the plot. + +![vputs](https://user-images.githubusercontent.com/25267873/115161873-f40e0f00-a097-11eb-8334-3d4f14b56766.png) + + +## chains + +```` +usage: chains [--calls] [--puts] [-m MIN_SP] [-M MAX_SP] +```` + +Display options chains. [Source: Tradier] + +* --calls : Flag to show calls only +* --puts : Flag to show puts only +* -m : Minimum strike price to consider. +* -M : Maximum strike price to consider. + + +chains diff --git a/gamestonk_terminal/options/chains_view.py b/gamestonk_terminal/options/chains_view.py new file mode 100644 index 00000000000..d1a99a9d1e3 --- /dev/null +++ b/gamestonk_terminal/options/chains_view.py @@ -0,0 +1,211 @@ +"""Helper functions for the options menu""" +__docformat__ = "numpy" + +import argparse +from typing import List +import requests +import pandas as pd +import numpy as np +from tabulate import tabulate +from gamestonk_terminal.config_terminal import TRADIER_TOKEN +from gamestonk_terminal.helper_funcs import ( + check_non_negative, + parse_known_args_and_warn, +) + +option_columns = [ + "bid", + "ask", + "strike", + "bidsize", + "asksize", + "volume", + "open_interest", + "option_type", +] +greek_columns = ["delta", "gamma", "theta", "vega", "ask_iv", "bid_iv", "mid_iv"] +df_columns = option_columns + greek_columns +column_map = {"mid_iv": "iv", "open_interest": "oi", "volume": "vol"} + + +def process_chains(response: requests.models.Response) -> pd.DataFrame: + """ + Function to take in the requests.get and return a DataFrame + Parameters + ---------- + response: requests.models.Response + This is the respnose from tradier api. + + Returns + ------- + opt_chain: pd.DataFrame + Dataframe with all available options + """ + json_response = response.json() + options = json_response["options"]["option"] + + opt_chain = pd.DataFrame(columns=df_columns) + for idx, option in enumerate(options): + data = [option[col] for col in option_columns] + data += [option["greeks"][col] for col in greek_columns] + opt_chain.loc[idx, :] = data + + return opt_chain + + +def get_option_chains(symbol: str, expiry: str) -> pd.DataFrame: + """ + + Parameters + ---------- + symbol : str + Ticker to get options for + expiry : str + Expiration date in the form of "YYYY-MM-DD" + + Returns + ------- + chains: pd.DataFrame + Dataframe with options for the gievn Symbol and Expiration date + """ + params = {"symbol": symbol, "expiration": expiry, "greeks": "true"} + + headers = {"Authorization": f"Bearer {TRADIER_TOKEN}", "Accept": "application/json"} + + response = requests.get( + "https://sandbox.tradier.com/v1/markets/options/chains", + params=params, + headers=headers, + ) + if response.status_code == 401: + print("Error in request.get -- check token") + return + + chains = process_chains(response) + return chains + + +def display_chains(symbol: str, expiry: str, other_args: List[str]): + + parser = argparse.ArgumentParser( + prog="chains", + add_help=False, + description="Display option chains [Source: Tradier]", + ) + + parser.add_argument( + "--calls", + action="store_true", + default=False, + dest="calls_only", + help="Flag to show calls only", + ) + + parser.add_argument( + "--puts", + action="store_true", + default=False, + dest="puts_only", + help="Flag to show puts only", + ) + parser.add_argument( + "-m", + "--min", + dest="min_sp", + type=check_non_negative, + default=-1, + help="minimum strike price to consider.", + ) + parser.add_argument( + "-M", + "--max", + dest="max_sp", + type=check_non_negative, + default=-1, + help="maximum strike price to consider.", + ) + + chains_df = get_option_chains(symbol, expiry) + + default_columns = [ + "mid_iv", + "delta", + "volume", + "open_interest", + "bid", + "ask", + "strike", + "option_type", + ] + chains_df = chains_df[default_columns].rename(columns=column_map) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + if ns_parser.max_sp == -1 and ns_parser.min_sp == -1: + min_strike = np.percentile(chains_df["strike"], 25) + max_strike = np.percentile(chains_df["strike"], 75) + else: + min_strike = ns_parser.min_sp + max_strike = ns_parser.max_sp + + chains_df = chains_df[chains_df["strike"] >= min_strike] + chains_df = chains_df[chains_df["strike"] <= max_strike] + calls_df = chains_df[chains_df.option_type == "call"].drop( + columns=["option_type"] + ) + puts_df = chains_df[chains_df.option_type == "put"].drop( + columns=["option_type"] + ) + + if ns_parser.calls_only: + print( + tabulate( + calls_df, + headers=calls_df.columns, + tablefmt="grid", + showindex=False, + floatfmt=".2f", + ) + ) + return + + if ns_parser.puts_only: + print( + tabulate( + puts_df, + headers=puts_df.columns, + tablefmt="grid", + showindex=False, + floatfmt=".2f", + ) + ) + return + + puts_df = puts_df[puts_df.columns[::-1]] + chain_table = calls_df.merge(puts_df, on="strike") + + headers = [ + col.strip("_x") + if col.endswith("_x") + else col.strip("_y") + if col.endswith("_y") + else col + for col in chain_table.columns + ] + print( + tabulate( + chain_table, + headers=headers, + tablefmt="fancy_grid", + showindex=False, + floatfmt=".2f", + ) + ) + print("") + + except Exception as e: + print(e) + print("") + return diff --git a/gamestonk_terminal/options/op_controller.py b/gamestonk_terminal/options/op_controller.py index 9459833b180..4a37d485327 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, chains_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", "chains"] - 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", @@ -39,7 +38,7 @@ class OptionsController: parser = argparse.ArgumentParser( add_help=False, prog="exp", - description="""See expiry dates.""", + description="""See/set expiry dates. [Source: Yahoo Finance]""", ) parser.add_argument( "-d", @@ -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,11 @@ 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("") + print(" chains display option chains") print("") return @@ -121,26 +125,50 @@ 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 call_chains(self, other_args): + chains_view.display_chains(self.ticker, self.expiry_date, other_args) + + +def menu(ticker: str, last_adj_close_price: float): + """ Options Menu. """ 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..4d32126c682 --- /dev/null +++ b/gamestonk_terminal/options/volume_view.py @@ -0,0 +1,465 @@ +""" Volume view """ +__docformat__ = "numpy" + +from bisect import bisect_left +from typing import List +import argparse +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np +import seaborn as sns + +from gamestonk_terminal.helper_funcs import ( + plot_autoscale, + 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", + width=0.45, + ) + plt.bar(df_calls["strike"], df_calls["volume"], color="green", width=0.45) + + plt.title(f"{ticker} calls volumes for {exp_date} ") + plt.legend(["Stock Price", "Open Interest", "Volume"]) + 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", + width=0.45, + ) + plt.bar(df_puts["strike"], -df_puts["volume"], color="red", width=0.45) + + plt.title(f"{ticker} puts volumes for {exp_date} ") + plt.legend(["Stock Price", "Open Interest", "Volume"]) + 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..3e9e44d4603 --- /dev/null +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -0,0 +1,224 @@ +# Portfolio Optimization + +This menu aims to optimize a portfolio of pre-loaded stocks, and the usage of the following commands along with an example will be exploited below. + +* [add](#add) + * add ticker to optimize +* [select](#select) + * overwrite current tickers with new tickers +* [equal](#equal) + * equally weighted +* [mktcap](#mktcap) + * weighted according to market cap (property marketCap) +* [dividend](#dividend) + * weighted according to dividend yield (property dividendYield) +* [property](#property) + * weight according to selected info property + +[Mean Variance Optimization](#Mean_Variance_Optimization) + +* [maxsharpe](#maxsharpe) + * optimizes for maximal Sharpe ratio (a.k.a the tangency portfolio) +* [minvol](#minvol) + * optimizes for minimum volatility +* [maxquadutil](#maxquadutil) + * maximises the quadratic utility, given some risk aversion +* [effret](#effret) + * maximises return for a given target risk +* [effrisk](#effrisk) + * minimises risk for a given target return +* [ef](#ef) + * show the efficient frontier + + +### add + +```text +add [-t ADD_TICKERS] +``` + +Add/Select tickers for portfolio to be optimized. + +* -t : Tickers to be used in the portfolio to optimize +* + +### select + +```text +select [-t ADD_TICKERS] +``` + +Add/Select tickers for portfolio to be optimized. + +* -t : Tickers to be used in the portfolio to optimize + + +### equal + +```text +equal [-v VALUE] [--pie] +``` + +Returns an equally weighted portfolio + +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights + +![equal](https://user-images.githubusercontent.com/25267873/114958292-9771e080-9e5a-11eb-9de6-1c2dfbe1e27b.png) + + +### mktcap + +```text +property -p marketCap [-v VALUE] [--pie] +``` + +Returns a portfolio that is weighted based on market cap info + +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights + +![mktCap](https://user-images.githubusercontent.com/25267873/115084151-57fdcf80-9f00-11eb-885c-bfe93de0eb6b.png) + + +### dividend + +```text +property -p dividend [-v VALUE] [--pie] +``` + +Returns a portfolio that is weighted based on dividend yield info + +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights + +Captura de ecrã 2021-04-16, às 22 11 41 + + +### property + +```text +property [-p PROPERTY] [-v VALUE] [--pie] +``` + +Returns a portfolio that is weighted based on a selected property info + +* -p : Property info to weigh. Use one of: previousClose, regularMarketOpen, twoHundredDayAverage, trailingAnnualDividendYield, payoutRatio, volume24Hr, regularMarketDayHigh, navPrice, averageDailyVolume10Day, totalAssets, regularMarketPreviousClose, fiftyDayAverage, trailingAnnualDividendRate, open, toCurrency, averageVolume10days, expireDate, yield, algorithm, dividendRate, exDividendDate, beta, circulatingSupply, regularMarketDayLow, priceHint, currency, trailingPE, regularMarketVolume, lastMarket, maxSupply, openInterest, marketCap, volumeAllCurrencies, strikePrice, averageVolume, priceToSalesTrailing12Months, dayLow, ask, ytdReturn, askSize, volume, fiftyTwoWeekHigh, forwardPE, fromCurrency, fiveYearAvgDividendYield, fiftyTwoWeekLow, bid, dividendYield, bidSize, dayHigh, annualHoldingsTurnover, enterpriseToRevenue, beta3Year, profitMargins, enterpriseToEbitda, 52WeekChange, morningStarRiskRating, forwardEps, revenueQuarterlyGrowth, sharesOutstanding, fundInceptionDate, annualReportExpenseRatio, bookValue, sharesShort, sharesPercentSharesOut, fundFamily, lastFiscalYearEnd, heldPercentInstitutions, netIncomeToCommon, trailingEps, lastDividendValue, SandP52WeekChange, priceToBook, heldPercentInsiders, shortRatio, sharesShortPreviousMonthDate, floatShares, enterpriseValue, threeYearAverageReturn, lastSplitFactor, legalType, lastDividendDate, morningStarOverallRating, earningsQuarterlyGrowth, pegRatio, lastCapGain, shortPercentOfFloat, sharesShortPriorMonth, impliedSharesOutstanding, fiveYearAverageReturn, and regularMarketPrice. +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights + +![marketCap](https://user-images.githubusercontent.com/25267873/114958294-980a7700-9e5a-11eb-9db4-d06e6a1e8981.png) + + +## Mean Variance Optimization + +These approaches are based off of the efficient frontier approach, which is meant to solve the following optimization problem. + +

+ +

+ +* Where S is the covariance matrix between stocks and R is the expected returns. +* The condition that all weights add up to 1 just implies that you want to have a net long portfolio (with no margin). +* A long-short portfolio can have negative weights and usually wants to have everything add up to 0 for a market-neutral strategy. + +Currently, we do not allow for changing risk models or adding constraints. +If there is something specific, please submit a feature request, or if you can write it, feel free to add a PR! +All of these commands use [PyPortFolioOpt](#https://pyportfolioopt.readthedocs.io/en/latest/index.html) package. + + +### maxsharpe + +```` +usage: max_sharpe [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-v VALUE] [--pie] [-r RISK_FREE_RATE] +```` + +Maximise the Sharpe Ratio. The result is also referred to as the tangency portfolio, as it is the portfolio for which the capital market line is tangent to the efficient frontier. This is a convex optimization problem after making a certain variable substitution. See Cornuejols and Tutuncu (2006) for more. The sharpe ratio is defined as (Mean Returns - Risk Free Rate)/(Standard Deviation of Returns). + +* -p : Amount of time to retrieve data from yfinance. Default: 3mo. +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights + +maxsharpe + + +### minvol + +```` +usage: minvol [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-v VALUE] [--pie] +```` + +This portfolio minimizes the total volatility, which also means it has the smallest returns among the efficient frontier. + +* -p : Amount of time to retrieve data from yfinance. Default: 3mo. +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights + +![min_vol](https://user-images.githubusercontent.com/25267873/114958290-96d94a00-9e5a-11eb-9b7c-deba8a8e7249.png) +min_vol2 + + +### maxquadutil + +```` +usage: maxquadutil [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-v VALUE] [--pie] [-r RISK_AVERSION] [-n] +```` + +Maximises the quadratic utility, given some risk aversion. + +* -p : Amount of time to retrieve data from yfinance. Default: 3mo. +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights +* -r : Risk aversion parameter +* -n : Whether the portfolio should be market neutral (weights sum to zero). Defaults: False. + +maxquadutil + + +### effret + +```` +usage: effret [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-v VALUE] [--pie] [-t TARGET_RETURN] [-n] +```` + +Calculate the 'Markowitz portfolio', minimising volatility for a given target return. + +* -p : Amount of time to retrieve data from yfinance. Default: 3mo. +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights +* -t : The desired return of the resulting portfolio +* -n : Whether the portfolio should be market neutral (weights sum to zero). Defaults: False. + +effret + + +### effrisk + +```` +usage: effrisk [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-v VALUE] [--pie] [-t TARGET_VOLATILITY] [-n] +```` + +Maximise return for a target risk. The resulting portfolio will have a volatility less than the target (but not guaranteed to be equal). + +* -p : Amount of time to retrieve data from yfinance. Default: 3mo. +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights +* -t : The desired maximum volatility of the resultingportfolio +* -n : Whether the portfolio should be market neutral (weights sum to zero). Defaults: False. + +![effrisk](https://user-images.githubusercontent.com/25267873/114958284-95a81d00-9e5a-11eb-8626-988505db8890.png) +effrisk2 + + +### ef + +```` +usage: ef [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-n N_PORT] +```` + +This function plots random portfolios based on their risk and returns and shows the efficient frontier. + +* -n : Number of portfolios to simulate. Default 300. +* -p : Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo. + +![ef](https://user-images.githubusercontent.com/25267873/114958281-93de5980-9e5a-11eb-967b-d37ddd4f3e73.png) 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..f88ad80463c --- /dev/null +++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py @@ -0,0 +1,279 @@ +""" 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 + +l_valid_property_infos = [ + "previousClose", + "regularMarketOpen", + "twoHundredDayAverage", + "trailingAnnualDividendYield", + "payoutRatio", + "volume24Hr", + "regularMarketDayHigh", + "navPrice", + "averageDailyVolume10Day", + "totalAssets", + "regularMarketPreviousClose", + "fiftyDayAverage", + "trailingAnnualDividendRate", + "open", + "toCurrency", + "averageVolume10days", + "expireDate", + "yield", + "algorithm", + "dividendRate", + "exDividendDate", + "beta", + "circulatingSupply", + "regularMarketDayLow", + "priceHint", + "currency", + "trailingPE", + "regularMarketVolume", + "lastMarket", + "maxSupply", + "openInterest", + "marketCap", + "volumeAllCurrencies", + "strikePrice", + "averageVolume", + "priceToSalesTrailing12Months", + "dayLow", + "ask", + "ytdReturn", + "askSize", + "volume", + "fiftyTwoWeekHigh", + "forwardPE", + "fromCurrency", + "fiveYearAvgDividendYield", + "fiftyTwoWeekLow", + "bid", + "dividendYield", + "bidSize", + "dayHigh", + "annualHoldingsTurnover", + "enterpriseToRevenue", + "beta3Year", + "profitMargins", + "enterpriseToEbitda", + "52WeekChange", + "morningStarRiskRating", + "forwardEps", + "revenueQuarterlyGrowth", + "sharesOutstanding", + "fundInceptionDate", + "annualReportExpenseRatio", + "bookValue", + "sharesShort", + "sharesPercentSharesOut", + "fundFamily", + "lastFiscalYearEnd", + "heldPercentInstitutions", + "netIncomeToCommon", + "trailingEps", + "lastDividendValue", + "SandP52WeekChange", + "priceToBook", + "heldPercentInsiders", + "shortRatio", + "sharesShortPreviousMonthDate", + "floatShares", + "enterpriseValue", + "threeYearAverageReturn", + "lastSplitFactor", + "legalType", + "lastDividendDate", + "morningStarOverallRating", + "earningsQuarterlyGrowth", + "pegRatio", + "lastCapGain", + "shortPercentOfFloat", + "sharesShortPriorMonth", + "impliedSharesOutstanding", + "fiveYearAverageReturn", + "regularMarketPrice", +] + + +def check_valid_property_type(property: str) -> str: + """ Check that the property selected is valid """ + if property in l_valid_property_infos: + return property + else: + raise argparse.ArgumentTypeError(f"{property} is not a valid info") + + +def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFrame: + """Get adjusted closing price for each stock in the list + + 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[:4] if s.find(".") == 1 else "" + s[:5] + ) + " %" + print(pd.DataFrame(weight_df["weight"]).to_string(header=False)) + else: + print(weight_df.to_string(header=False)) + + +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, title_opt: str): + """Show a pie chart of holdings + + Parameters + ---------- + weights: dict + Weights to display, where keys are tickers, and values are either weights or values if -v specified + title: str + Title to be used on the plot title + """ + if not weights: + return + + 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) + + 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"), + wedgeprops={"linewidth": 3.0, "edgecolor": "white"}, + normalize=True, + ) + plt.setp(autotexts, color="white", fontweight="bold") + else: + wedges, _, autotexts = plt.pie( + sizes, + labels=stocks, + autopct="", + textprops=dict(color="k"), + wedgeprops={"linewidth": 3.0, "edgecolor": "white"}, + normalize=True, + ) + plt.setp(autotexts, color="white", fontweight="bold") + for i, a in enumerate(autotexts): + if sizes[i] / total_size > 0.05: + a.set_text(f"{sizes[i]:.2f}") + else: + a.set_text("") + + plt.axis("equal") + + leg1 = plt.legend( + wedges, + [str(s) for s in stocks], + title=" Ticker", + loc="upper left", + bbox_to_anchor=(0.80, 0, 0.5, 1), + frameon=False, + ) + leg2 = plt.legend( + wedges, + [ + f"{' ' if ((100*s/total_size) < 10) else ''}{100*s/total_size:.2f}%" + for s in sizes + ], + title=" ", + loc="upper left", + handlelength=0, + bbox_to_anchor=(0.91, 0, 0.5, 1), + frameon=False, + ) + plt.gca().add_artist(leg1) + plt.gca().add_artist(leg2) + + plt.setp(autotexts, size=8, weight="bold") + + plt.gca().set_title(title_opt, pad=20) + + if gtff.USE_ION: + plt.ion() + + plt.tight_layout() + + plt.show() + print("") diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py new file mode 100644 index 00000000000..6c61666d5bf --- /dev/null +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -0,0 +1,790 @@ +""" 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, + check_non_negative, +) +from gamestonk_terminal.portfolio_optimization.optimizer_helper import ( + process_stocks, + prepare_efficient_frontier, + pie_chart_weights, + display_weights, + check_valid_property_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", +] + +d_period = { + "1d": "[1 Day]", + "5d": "[5 Days]", + "1mo": "[1 Month]", + "3mo": "[3 Months]", + "6mo": "[6 Months]", + "1y": "[1 Year]", + "2y": "[2 Years]", + "5y": "[5 Years]", + "10y": "[10 Years]", + "ytd": "[Year-to-Date]", + "max": "[All-time]", +} + + +def equal_weight(stocks: List[str], other_args: List[str]): + """ + 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", + description="Returns an equally weighted portfolio", + ) + + 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 + + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return + + values = {} + for stock in stocks: + values[stock] = ns_parser.value * round(1 / len(stocks), 5) + if ns_parser.pie: + pie_chart_weights(values, "Equally Weighted Portfolio") + else: + display_weights(values) + print("") + + except Exception as e: + print(e) + print("") + + +def property_weighting(stocks: List[str], other_args: List[str]): + """Weighted portfolio where each weight is the relative fraction of a specified info property. + + Parameters + ---------- + stocks: List[str] + List of tickers to be included in optimization + other_args : List[str] + Command line arguments to be processed with argparse + + Returns + ------- + weights: dict + Dictionary of weights where keys are the tickers + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="property", + description="Returns a portfolio that is weighted based on a selected property info", + ) + parser.add_argument( + "-p", + "--property", + required=True if "-h" not in other_args else False, + type=check_valid_property_type, + dest="property", + help="""Property info to weigh. Use one of: + previousClose, regularMarketOpen, twoHundredDayAverage, trailingAnnualDividendYield, + payoutRatio, volume24Hr, regularMarketDayHigh, navPrice, averageDailyVolume10Day, totalAssets, + regularMarketPreviousClose, fiftyDayAverage, trailingAnnualDividendRate, open, toCurrency, averageVolume10days, + expireDate, yield, algorithm, dividendRate, exDividendDate, beta, circulatingSupply, regularMarketDayLow, + priceHint, currency, trailingPE, regularMarketVolume, lastMarket, maxSupply, openInterest, marketCap, + volumeAllCurrencies, strikePrice, averageVolume, priceToSalesTrailing12Months, dayLow, ask, ytdReturn, askSize, + volume, fiftyTwoWeekHigh, forwardPE, fromCurrency, fiveYearAvgDividendYield, fiftyTwoWeekLow, bid, dividendYield, + bidSize, dayHigh, annualHoldingsTurnover, enterpriseToRevenue, beta3Year, profitMargins, enterpriseToEbitda, + 52WeekChange, morningStarRiskRating, forwardEps, revenueQuarterlyGrowth, sharesOutstanding, fundInceptionDate, + annualReportExpenseRatio, bookValue, sharesShort, sharesPercentSharesOut, fundFamily, lastFiscalYearEnd, + heldPercentInstitutions, netIncomeToCommon, trailingEps, lastDividendValue, SandP52WeekChange, priceToBook, + heldPercentInsiders, shortRatio, sharesShortPreviousMonthDate, floatShares, enterpriseValue, + threeYearAverageReturn, lastSplitFactor, legalType, lastDividendDate, morningStarOverallRating, + earningsQuarterlyGrowth, pegRatio, lastCapGain, shortPercentOfFloat, sharesShortPriorMonth, + impliedSharesOutstanding, fiveYearAverageReturn, and regularMarketPrice.""", + ) + parser.add_argument( + "-v", + "--value", + 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: + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-p") + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return + + weights = {} + prop = {} + prop_sum = 0 + for stock in stocks: + stock_prop = yf.Ticker(stock).info[ns_parser.property] + if stock_prop is None: + stock_prop = 0 + prop[stock] = stock_prop + prop_sum += stock_prop + + if prop_sum == 0: + print(f"No {ns_parser.property} was found on list of tickers provided") + print("") + return + + for k, v in prop.items(): + weights[k] = round(v / prop_sum, 5) * ns_parser.value + + if ns_parser.pie: + pie_chart_weights( + weights, "Weighted Portfolio based on " + ns_parser.property + ) + else: + display_weights(weights) + print("") + + except Exception as e: + print(e) + print("") + return + + +def max_sharpe(stocks: List[str], other_args: List[str]): + """ + Return a portfolio that maximises the Sharpe Ratio + + Parameters + ---------- + stocks: List[str] + List of the stocks to be included in the weights + + Returns + ------- + weights: dict + Dictionary of weights where keys are the tickers. + """ + parser = argparse.ArgumentParser( + add_help=False, prog="maxsharpe", description="Maximise the Sharpe Ratio" + ) + parser.add_argument( + "-p", + "--period", + default="3mo", + dest="period", + help="period to get yfinance data from", + choices=period_choices, + ) + parser.add_argument( + "-v", + "--value", + dest="value", + help="Amount to allocate to portfolio", + type=float, + default=1.0, + ) + parser.add_argument( + "--pie", + action="store_true", + dest="pie", + default=False, + help="Display a pie chart for weights", + ) + + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-r") + parser.add_argument( + "-r", + "--risk-free-rate", + type=float, + dest="risk_free_rate", + default=0.02, + help="""Risk-free rate of borrowing/lending. The period of the risk-free rate + should correspond to the frequency of expected returns.""", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return None + + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return + + period = ns_parser.period + stock_prices = process_stocks(stocks, period) + ef = prepare_efficient_frontier(stock_prices) + + sp = d_period[ns_parser.period] + + ef_opt = dict(ef.max_sharpe(ns_parser.risk_free_rate)) + s_title = f"{sp} Weights that maximize Sharpe ratio with risk free level of {ns_parser.risk_free_rate}" + + weights = { + key: ns_parser.value * round(value, 5) for key, value in ef_opt.items() + } + + if ns_parser.pie: + pie_chart_weights(weights, s_title) + else: + print(s_title) + display_weights(weights) + print("") + + ef.portfolio_performance(verbose=True) + print("") + + except Exception as e: + print(e) + print("") + return + + +def min_volatility(stocks: List[str], other_args: List[str]): + """ + Return a portfolio that optimizes for minimum volatility + + Parameters + ---------- + stocks: List[str] + List of the stocks to be included in the weights + + Returns + ------- + weights: dict + Dictionary of weights where keys are the tickers. + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="min_volatility", + description="Optimizes for minimum volatility", + ) + parser.add_argument( + "-p", + "--period", + default="3mo", + dest="period", + help="period to get yfinance data from", + choices=period_choices, + ) + parser.add_argument( + "-v", + "--value", + dest="value", + help="Amount to allocate to portfolio", + type=float, + default=1.0, + ) + parser.add_argument( + "--pie", + action="store_true", + dest="pie", + default=False, + help="Display a pie chart for weights", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return None + + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return + + period = ns_parser.period + stock_prices = process_stocks(stocks, period) + ef = prepare_efficient_frontier(stock_prices) + + sp = d_period[ns_parser.period] + + ef_opt = dict(ef.min_volatility()) + s_title = f"{sp} Weights that minimize volatility" + + weights = { + key: ns_parser.value * round(value, 5) for key, value in ef_opt.items() + } + + if ns_parser.pie: + pie_chart_weights(weights, s_title) + else: + print(s_title) + display_weights(weights) + print("") + + ef.portfolio_performance(verbose=True) + print("") + + except Exception as e: + print(e) + print("") + return + + +def max_quadratic_utility(stocks: List[str], other_args: List[str]): + """ + Return a portfolio that maximises the quadratic utility, given some risk aversion + + Parameters + ---------- + stocks: List[str] + List of the stocks to be included in the weights + + Returns + ------- + weights: dict + Dictionary of weights where keys are the tickers. + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="max_quadratic_utility", + description="Maximises the quadratic utility, given some risk aversion", + ) + parser.add_argument( + "-p", + "--period", + default="3mo", + dest="period", + help="period to get yfinance data from", + choices=period_choices, + ) + parser.add_argument( + "-v", + "--value", + dest="value", + help="Amount to allocate to portfolio", + type=float, + default=1.0, + ) + parser.add_argument( + "-n", + "--market-neutral", + action="store_true", + default=False, + dest="market_neutral", + help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False. + Requires negative lower weight bound.""", + ) + if "-n" not in other_args: + parser.add_argument( + "--pie", + action="store_true", + dest="pie", + default=False, + help="Display a pie chart for weights. Only if neutral flag is left False.", + ) + + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-r") + + parser.add_argument( + "-r", + "--risk-aversion", + type=float, + dest="risk_aversion", + default=1, + help="risk aversion parameter", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return None + + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return + + period = ns_parser.period + stock_prices = process_stocks(stocks, period) + ef = prepare_efficient_frontier(stock_prices) + + sp = d_period[ns_parser.period] + + ef_opt = dict( + ef.max_quadratic_utility(ns_parser.risk_aversion, ns_parser.market_neutral) + ) + s_title = f"{sp} Weights that maximise the quadratic utility with risk aversion of {ns_parser.risk_aversion}" + + weights = { + key: ns_parser.value * round(value, 5) for key, value in ef_opt.items() + } + + if not ns_parser.market_neutral: + if ns_parser.pie: + pie_chart_weights(weights, s_title) + ef.portfolio_performance(verbose=True) + print("") + return + + print(s_title) + display_weights(weights) + print("") + ef.portfolio_performance(verbose=True) + print("") + + except Exception as e: + print(e) + print("") + return + + +def efficient_risk(stocks: List[str], other_args: List[str]): + """ + Return a portfolio that maximises return for a target risk. The resulting portfolio will have + a volatility less than the target (but not guaranteed to be equal) + + Parameters + ---------- + stocks: List[str] + List of the stocks to be included in the weights + + Returns + ------- + weights: dict + Dictionary of weights where keys are the tickers. + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="effrisk", + description="""Maximise return for a target risk. The resulting portfolio will have + a volatility less than the target (but not guaranteed to be equal)""", + ) + parser.add_argument( + "-p", + "--period", + default="3mo", + dest="period", + help="period to get yfinance data from", + choices=period_choices, + ) + parser.add_argument( + "-v", + "--value", + dest="value", + help="Amount to allocate to portfolio", + type=float, + default=1.0, + ) + parser.add_argument( + "-n", + "--market-neutral", + action="store_true", + default=False, + dest="market_neutral", + help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False. + Requires negative lower weight bound.""", + ) + if "-n" not in other_args: + parser.add_argument( + "--pie", + action="store_true", + dest="pie", + default=False, + help="Display a pie chart for weights. Only if neutral flag is left False.", + ) + + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-t") + parser.add_argument( + "-t", + "--target-volatility", + type=float, + dest="target_volatility", + default=0.1, + help="The desired maximum volatility of the resulting portfolio", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return None + + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return + + period = ns_parser.period + stock_prices = process_stocks(stocks, period) + ef = prepare_efficient_frontier(stock_prices) + + sp = d_period[ns_parser.period] + + ef_opt = dict( + ef.efficient_risk(ns_parser.target_volatility, ns_parser.market_neutral) + ) + s_title = f"{sp} Weights that maximize return with a maximum volatility of {ns_parser.target_volatility}" + + weights = { + key: ns_parser.value * round(value, 5) for key, value in ef_opt.items() + } + + if not ns_parser.market_neutral: + if ns_parser.pie: + pie_chart_weights(weights, s_title) + ef.portfolio_performance(verbose=True) + print("") + return + + print(s_title) + display_weights(weights) + print("") + ef.portfolio_performance(verbose=True) + print("") + + except Exception as e: + print(e) + print("") + return + + +def efficient_return(stocks: List[str], other_args: List[str]): + """ + Return a portfolio that minimises volatility for a given target return ('Markowitz portfolio') + + Parameters + ---------- + stocks: List[str] + List of the stocks to be included in the weights + + Returns + ------- + weights: dict + Dictionary of weights where keys are the tickers. + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="effret", + description="Calculate the 'Markowitz portfolio', minimising volatility for a given target return", + ) + parser.add_argument( + "-p", + "--period", + default="3mo", + dest="period", + help="period to get yfinance data from", + choices=period_choices, + ) + parser.add_argument( + "-v", + "--value", + dest="value", + help="Amount to allocate to portfolio", + type=float, + default=1.0, + ) + parser.add_argument( + "-n", + "--market-neutral", + action="store_true", + default=False, + dest="market_neutral", + help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False. + Requires negative lower weight bound.""", + ) + if "-n" not in other_args: + parser.add_argument( + "--pie", + action="store_true", + dest="pie", + default=False, + help="Display a pie chart for weights. Only if neutral flag is left False.", + ) + + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-t") + parser.add_argument( + "-t", + "--target-return", + type=float, + dest="target_return", + default=0.1, + help="the desired return of the resulting portfolio", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return None + + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return + + period = ns_parser.period + stock_prices = process_stocks(stocks, period) + ef = prepare_efficient_frontier(stock_prices) + + sp = d_period[ns_parser.period] + + ef_opt = dict( + ef.efficient_return(ns_parser.target_return, ns_parser.market_neutral) + ) + s_title = f"{sp} Weights that minimise volatility for a given target return of {ns_parser.target_return}" + + weights = { + key: ns_parser.value * round(value, 5) for key, value in ef_opt.items() + } + + if not ns_parser.market_neutral: + if ns_parser.pie: + pie_chart_weights(weights, s_title) + ef.portfolio_performance(verbose=True) + print("") + return + + print(s_title) + display_weights(weights) + print("") + ef.portfolio_performance(verbose=True) + print("") + + except Exception as e: + print(e) + print("") + return + + +def show_ef(stocks: List[str], other_args: List[str]): + parser = argparse.ArgumentParser( + add_help=False, + prog="ef", + description="""This function plots random portfolios based + on their risk and returns and shows the efficient frontier.""", + ) + + parser.add_argument( + "-p", + "--period", + default="3mo", + dest="period", + help="period to get yfinance data from", + choices=period_choices, + ) + parser.add_argument( + "-n", + "--number-portfolios", + default=300, + type=check_non_negative, + dest="n_port", + help="number of portfolios to simulate", + ) + + try: + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-n") + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\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(f"Efficient Frontier simulating {ns_parser.n_port} portfolios") + 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..c66ca63e846 --- /dev/null +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -0,0 +1,243 @@ +"""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 + + +class PortfolioOptimization: + + CHOICES = [ + "help", + "q", + "quit", + "select", + "add", + "equal", + "mktcap", + "dividend", + "property", + "maxsharpe", + "minvol", + "effret", + "effrisk", + "maxquadutil", + "ef", + "ca", + "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(" equal equally weighted") + print(" mktcap weighted according to market cap (property marketCap)") + print( + " dividend weighted according to dividend yield (property dividendYield)" + ) + print(" property weight according to selected info property") + print("") + print("Mean Variance Optimization:") + print( + " maxsharpe optimizes for maximal Sharpe ratio (a.k.a the tangency portfolio)" + ) + print(" minvol optimizes for minimum volatility") + print( + " maxquadutil maximises the quadratic utility, given some risk aversion" + ) + print(" effret maximises return for a given target risk") + print(" effrisk minimises risk for a given target return") + print("") + print(" ef show the efficient frontier") + print("") + if tickers: + print(" > ca comparison analysis for selected tickers") + print("") + + def switch(self, an_input: str): + """Process and dispatch input + + Returns + ------- + True, False or None + False - quit the menu + True - quit the program + None - continue in the menu + """ + (known_args, other_args) = self.po_parser.parse_known_args(an_input.split()) + + return getattr( + self, "call_" + known_args.cmd, lambda: "Command not recognized!" + )(other_args) + + def call_help(self, _): + """Process Help command""" + self.print_help(self.tickers) + + def call_q(self, _): + """Process Q command - quit the menu""" + return False + + def call_quit(self, _): + """Process Quit command - quit the program""" + return True + + def call_add(self, other_args: List[str]): + """Process add command""" + self.add_stocks(self, other_args) + + def call_select(self, other_args: List[str]): + """Process select command""" + self.tickers = [] + self.add_stocks(self, other_args) + + def call_equal(self, other_args: List[str]): + """Process equal command""" + optimizer_view.equal_weight(self.tickers, other_args) + + def call_mktcap(self, other_args: List[str]): + """Process mktcap command""" + other_args.insert(0, "marketCap") + optimizer_view.property_weighting(self.tickers, other_args) + + def call_dividend(self, other_args: List[str]): + """Process dividend command""" + other_args.insert(0, "dividendYield") + optimizer_view.property_weighting(self.tickers, other_args) + + def call_property(self, other_args: List[str]): + """Process property command""" + optimizer_view.property_weighting(self.tickers, other_args) + + def call_maxsharpe(self, other_args: List[str]): + """Process maxsharpe command""" + optimizer_view.max_sharpe(self.tickers, other_args) + + def call_minvol(self, other_args: List[str]): + """Process minvol command""" + optimizer_view.min_volatility(self.tickers, other_args) + + def call_maxquadutil(self, other_args: List[str]): + """Process maxquadutil command""" + optimizer_view.max_quadratic_utility(self.tickers, other_args) + + def call_effrisk(self, other_args: List[str]): + """Process effrisk command""" + optimizer_view.efficient_risk(self.tickers, other_args) + + def call_effret(self, other_args: List[str]): + """Process effret command""" + optimizer_view.efficient_return(self.tickers, other_args) + + def call_ef(self, other_args): + """Process ef command""" + optimizer_view.show_ef(self.tickers, other_args) + + def call_yolo(self, _): + # Easter egg :) + print("DFV YOLO") + print({"GME": 200}) + print("") + + @staticmethod + def add_stocks(self, other_args: List[str]): + """ Add ticker or Select tickes for portfolio to be optimized """ + parser = argparse.ArgumentParser( + add_help=False, + prog="add/select", + description="""Add/Select tickers for portfolio to be optimized.""", + ) + parser.add_argument( + "-t", + "--tickers", + dest="add_tickers", + type=lambda s: [str(item).upper() for item in s.split(",")], + default=[], + help="tickers to be used in the portfolio to optimize.", + ) + 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) + + if self.tickers: + print( + f"\nCurrent Tickers: {('None', ', '.join(tickers))[bool(tickers)]}" + ) + + self.tickers = list(tickers) + + 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: + plt.close("all") + + 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/screener/README.md b/gamestonk_terminal/screener/README.md index b4f46eb644d..3135fb3f981 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](portfolio_optimization/README.md) + * portfolio optimization for last screened tickers ## 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 +```` diff --git a/gamestonk_terminal/screener/finviz_view.py b/gamestonk_terminal/screener/finviz_view.py index 4ca3bb96857..3c331be9649 100644 --- a/gamestonk_terminal/screener/finviz_view.py +++ b/gamestonk_terminal/screener/finviz_view.py @@ -214,12 +214,16 @@ def screener(other_args: List[str], loaded_preset: str, data_type: str): if isinstance(df_screen, pd.DataFrame): print(df_screen.to_string()) - print("") + print("") + return list(df_screen["Ticker"].values) + else: + print("") + return [] except Exception as e: print(e) print("") - return + return [] def view_signals(other_args: List[str]): diff --git a/gamestonk_terminal/screener/screener_controller.py b/gamestonk_terminal/screener/screener_controller.py index cc34b3e80ab..7edc438c501 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,11 +35,13 @@ class ScreenerController: "performance", "technical", "signals", + "po", ] def __init__(self): """Constructor""" self.preset = "template" + self.screen_tickers = [] self.scr_parser = argparse.ArgumentParser(add_help=False, prog="scr") self.scr_parser.add_argument( "cmd", @@ -69,6 +72,11 @@ class ScreenerController: print("") print(" signals view filter signals (e.g. -s top_gainers)") print("") + if self.screen_tickers: + print(f"Last screened tickers: {', '.join(self.screen_tickers)}") + print("") + print(" > po portfolio optimization for last screened tickers") + print("") @staticmethod def view_available_presets(other_args: List[str]): @@ -187,36 +195,42 @@ class ScreenerController: def call_historical(self, other_args: List[str]): """Process historical command""" - yahoo_finance_view.historical(other_args, self.preset) + self.screen_tickers = yahoo_finance_view.historical(other_args, self.preset) def call_overview(self, other_args: List[str]): """Process overview command""" - finviz_view.screener(other_args, self.preset, "overview") + self.screen_tickers = finviz_view.screener(other_args, self.preset, "overview") def call_valuation(self, other_args: List[str]): """Process valuation command""" - finviz_view.screener(other_args, self.preset, "valuation") + self.screen_tickers = finviz_view.screener(other_args, self.preset, "valuation") def call_financial(self, other_args: List[str]): """Process financial command""" - finviz_view.screener(other_args, self.preset, "financial") + self.screen_tickers = finviz_view.screener(other_args, self.preset, "financial") def call_ownership(self, other_args: List[str]): """Process ownership command""" - finviz_view.screener(other_args, self.preset, "ownership") + self.screen_tickers = finviz_view.screener(other_args, self.preset, "ownership") def call_performance(self, other_args: List[str]): """Process performance command""" - finviz_view.screener(other_args, self.preset, "performance") + self.screen_tickers = finviz_view.screener( + other_args, self.preset, "performance" + ) def call_technical(self, other_args: List[str]): """Process technical command""" - finviz_view.screener(other_args, self.preset, "technical") + self.screen_tickers = finviz_view.screener(other_args, self.preset, "technical") def call_signals(self, other_args: List[str]): """Process signals command""" finviz_view.view_signals(other_args) + def call_po(self, other_args: List[str]): + """Call the portfolio optimization menu with selected tickers""" + return po_controller.menu(self.screen_tickers) + def menu(): """Screener Menu""" diff --git a/gamestonk_terminal/screener/yahoo_finance_view.py b/gamestonk_terminal/screener/yahoo_finance_view.py index 6ad1c6e93bb..2a05ddbeec1 100644 --- a/gamestonk_terminal/screener/yahoo_finance_view.py +++ b/gamestonk_terminal/screener/yahoo_finance_view.py @@ -1,5 +1,6 @@ import argparse from typing import List +import random from pandas.plotting import register_matplotlib_converters import matplotlib.pyplot as plt import datetime @@ -119,10 +120,12 @@ def historical(other_args: List[str], preset_loaded: str): if len(l_stocks) > 10: print( - "We limit stocks shown to 10, as after this the plot becomes too noisy. " - "Also, we ran out of different colors." + "\nThe limit of stocks to compare with are 10. Hence, 10 random similar stocks will be displayed.", + "\nThe selected list will be:", ) - l_stocks = l_stocks[:10] + random.shuffle(l_stocks) + l_stocks = sorted(l_stocks[:10]) + print(", ".join(l_stocks)) while l_stocks: l_parsed_stocks = list() @@ -179,8 +182,11 @@ def historical(other_args: List[str], preset_loaded: str): plt.show() print("") + return l_parsed_stocks except SystemExit: print("Similar companies need to be provided", "\n") + return [] except Exception as e: print(e, "\n") + return [] diff --git a/gamestonk_terminal/technical_analysis/README.md b/gamestonk_terminal/technical_analysis/README.md index c35ebb6bdaa..0e5c511d850 100644 --- a/gamestonk_terminal/technical_analysis/README.md +++ b/gamestonk_terminal/technical_analysis/README.md @@ -43,7 +43,7 @@ This menu aims to perform a technical analysis on a pre-loaded ticker chart, and * [obv](#obv) - on balance volume -**S/O to https://github.com/twopirllc/pandas-ta** _Owing to this library, it is fairly easy to add other technical indicators. So, let me know if there's any that you would like. Personally I use mostly these ones, hence why I didn't add more._ +**S/O to https://github.com/twopirllc/pandas-ta** _Owing to this library, it is fairly easy to add other technical indicators. So, let us know if there's any other that you would like._ ## view ``` diff --git a/poetry.lock b/poetry.lock index b20746d369b..0a01734d46e 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" @@ -1603,7 +1643,7 @@ name = "patsy" version = "0.5.1" description = "A Python package for describing statistical models and for building design matrices." category = "main" -optional = true +optional = false python-versions = "*" [package.dependencies] @@ -1668,7 +1708,7 @@ name = "pmdarima" version = "1.8.0" description = "Python's forecast::auto.arima equivalent" category = "main" -optional = true +optional = false python-versions = ">=3.6" [package.dependencies] @@ -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" @@ -2360,7 +2441,7 @@ name = "statsmodels" version = "0.12.2" description = "Statistical computations and models for Python" category = "main" -optional = true +optional = false python-versions = ">=3.6" [package.dependencies] @@ -2379,7 +2460,7 @@ name = "tabulate" version = "0.8.9" description = "Pretty-print tabular data" category = "main" -optional = true +optional = false python-versions = "*" [package.extras] @@ -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 = "623e5809320b1fb71c692bdd6cdcb9ea3419f3a5b3592e6c755816bd493769bc" [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"}, @@ -4417,6 +4581,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..12ccd7ebcc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ papermill = "^2.3.2" torch = {version = "1.7.1", optional = true} transformers = {version = "3.5.1", optional = true} fbprophet = {version = "0.6", optional = true} -pmdarima = {version = "1.8.0", optional = true} +pmdarima = {version = "1.8.0", optional = false} tensorflow = {version = "2.4.1", optional = true} flair = {version = "0.7", optional = true} holidays = "^0.10" @@ -54,6 +54,9 @@ detecta = "^0.0.5" tradingview-ta = "^3.2.3" finvizfinance = "^0.9.3" Pillow = "^8.2.0" +pyportfolioopt = "^1.4.1" +statsmodels = "^0.12.2" +tabulate = "^0.8.9" [tool.poetry.dev-dependencies] pytest = "^6.2.2" 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 42437fa9707..2a54ea0287f 100644 --- a/terminal.py +++ b/terminal.py @@ -31,6 +31,7 @@ from gamestonk_terminal.residuals_analysis import ra_controller from gamestonk_terminal.portfolio import port_controller from gamestonk_terminal.cryptocurrency import crypto_controller from gamestonk_terminal.screener import screener_controller +from gamestonk_terminal.portfolio_optimization import po_controller # import warnings # warnings.simplefilter("always") @@ -88,7 +89,9 @@ def main(): "pa", "crypto", "ra", + "po", ] + menu_parser.add_argument("opt", choices=choices) completer = NestedCompleter.from_nested_dict({c: None for c in choices}) @@ -193,26 +196,51 @@ def main(): b_quit = mill.papermill_menu() elif ns_known_args.opt == "ba": - b_quit = ba_controller.menu(s_ticker, s_start) + b_quit = ba_controller.menu( + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, s_start + ) elif ns_known_args.opt == "res": - b_quit = rm.res_menu(s_ticker, s_start, s_interval) + b_quit = rm.res_menu( + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, + s_start, + s_interval, + ) elif ns_known_args.opt == "ca": b_quit = ca_controller.menu(df_stock, s_ticker, s_start, s_interval) elif ns_known_args.opt == "fa": - b_quit = fa_controller.menu(s_ticker, s_start, s_interval) + b_quit = fa_controller.menu( + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, + s_start, + s_interval, + ) elif ns_known_args.opt == "ta": - b_quit = ta_controller.menu(df_stock, s_ticker, s_start, s_interval) + b_quit = ta_controller.menu( + df_stock, + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, + s_start, + s_interval, + ) elif ns_known_args.opt == "dd": - b_quit = dd_controller.menu(df_stock, s_ticker, s_start, s_interval) + b_quit = dd_controller.menu( + df_stock, + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, + s_start, + s_interval, + ) elif ns_known_args.opt == "eda": if s_interval == "1440min": - b_quit = eda_controller.menu(df_stock, s_ticker, s_start, s_interval) + b_quit = eda_controller.menu( + df_stock, + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, + s_start, + s_interval, + ) else: df_stock = yf.download(s_ticker, start=s_start, progress=False) df_stock = df_stock.rename( @@ -228,10 +256,17 @@ def main(): df_stock.index.name = "date" s_interval = "1440min" - b_quit = eda_controller.menu(df_stock, s_ticker, s_start, s_interval) + b_quit = eda_controller.menu( + df_stock, + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, + s_start, + s_interval, + ) elif ns_known_args.opt == "op": - b_quit = op_controller.menu(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() @@ -242,6 +277,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: @@ -265,7 +303,12 @@ def main(): continue if s_interval == "1440min": - b_quit = pred_controller.menu(df_stock, s_ticker, s_start, s_interval) + b_quit = pred_controller.menu( + df_stock, + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, + s_start, + s_interval, + ) # If stock data is intradaily, we need to get data again as prediction # techniques work on daily adjusted data. By default we load data from # Alpha Vantage because the historical data loaded gives a larger @@ -283,7 +326,10 @@ def main(): df_stock_pred = df_stock_pred.sort_index(ascending=True) df_stock_pred = df_stock_pred[s_start:] b_quit = pred_controller.menu( - df_stock_pred, s_ticker, s_start, interval="1440min" + df_stock_pred, + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, + s_start, + s_interval="1440min", ) except Exception as e: print(e) @@ -291,7 +337,12 @@ def main(): return elif ns_known_args.opt == "ra": - b_quit = ra_controller.menu(df_stock, s_ticker, s_start, s_interval) + b_quit = ra_controller.menu( + df_stock, + s_ticker.split(".")[0] if "." in s_ticker else s_ticker, + s_start, + s_interval, + ) elif ns_known_args.opt == "scr": b_quit = screener_controller.menu()