diff --git a/README.md b/README.md index 54c5089fe7f..84a6007fdd1 100644 --- a/README.md +++ b/README.md @@ -257,10 +257,14 @@ These are the ones where a key is necessary: * Financial Modeling Prep: https://financialmodelingprep.com/developer * FRED: https://fred.stlouisfed.org/docs/api/api_key.html * News API: https://newsapi.org + * Tradier: https://developer.tradier.com/getting_started When these are obtained, don't forget to update [config_terminal.py](/gamestonk_terminal/config_terminal.py). -Alternatively, you can also set them to the following environment variables: GT_API_KEY_ALPHAVANTAGE, GT_API_KEY_FINANCIALMODELINGPREP, GT_API_KEY_QUANDL, GT_API_REDDIT_CLIENT_ID, GT_API_REDDIT_CLIENT_SECRET, GT_API_REDDIT_USERNAME, GT_API_REDDIT_USER_AGENT, GT_API_REDDIT_PASSWORD, GT_API_TWITTER_KEY, GT_API_TWITTER_SECRET_KEY, GT_API_TWITTER_BEARER_TOKEN, GT_API_POLYGON_KEY, GT_FRED_API_KEY, GT_API_NEWS_TOKEN. +Alternatively, you can also set them to the following environment variables: GT_API_KEY_ALPHAVANTAGE, +GT_API_KEY_FINANCIALMODELINGPREP, GT_API_KEY_QUANDL, GT_API_REDDIT_CLIENT_ID, GT_API_REDDIT_CLIENT_SECRET, GT_API_REDDIT_USERNAME, +GT_API_REDDIT_USER_AGENT, GT_API_REDDIT_PASSWORD, GT_API_TWITTER_KEY, GT_API_TWITTER_SECRET_KEY, +GT_API_TWITTER_BEARER_TOKEN, GT_API_POLYGON_KEY, GT_FRED_API_KEY, GT_API_NEWS_TOKEN, GT_TRADIER_TOKEN . Example: ``` diff --git a/ROADMAP.md b/ROADMAP.md index 86e76c6d2ac..82d5b910ed9 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,7 +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 top ratings updates (@hinxx) - [PR #350](https://github.com/DidierRLopes/GamestonkTerminal/pull/350) +* [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** @@ -111,6 +113,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** @@ -143,13 +146,17 @@ * [ ] Add Brokers (td, webull, etc) (@jmaslek) * [ ] Refactoring * [ ] Summaries / tear sheets (@jmaslek) + ___ -##Portfolio Optimization + +## Portfolio Optimization * [x] Basic Optimization through PyPortFolioOpt(@jmaslek) - [PR #329](https://github.com/DidierRLopes/GamestonkTerminal/pull/329) +* [x] Add command to maximise the quadratic utility(@didier) - [PR #349](https://github.com/DidierRLopes/GamestonkTerminal/pull/349) **NEXT** -* [ ] Decide what features should be present * [ ] Allow for more custom optimization constrains + + --- ## FRED @@ -190,6 +197,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 e41212591eb..192f21ca288 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,8 @@ Command|Description|Source `valuation` |valuation of sectors, industry, country |[Finviz](https://finviz.com) `performance` |performance of sectors, industry, country |[Finviz](https://finviz.com) `spectrum` |spectrum of sectors, industry, country |[Finviz](https://finviz.com) +`latest` |latest news |[Seeking Alpha](https://seekingalpha.com/) +`trending` |trending news |[Seeking Alpha](https://seekingalpha.com/) `ratings ` |top ratings updates |[MarketBeat](https://marketbeat.com)   @@ -243,16 +246,19 @@ Command|Description|Brokers   ## Portfolio Optimization [»](portfolio_optimization/README.md) -command|desciription + +Command|Description ------|------ -`equal_weight`|Returns equal weights for all selected stocks -`mkt_cap`| Returns weights that are weighted by market cap -`div_yield`|Returns weights that are weighted by dividend yield -`max_sharpe`| Returns portfolio of Efficient Frontier with maximum sharpe ratio -`min_vol`| Returns portfolio on Efficient Frontier with minimum volatility -`eff_risk`| Returns portfolio on Efficient Frontier that maximizes returns at a given risk -`eff_ret`| Returns portfolio on Efficient Frontier that minimizes risk at a given return level -`show_eff`|Plots random portfolios and shows the Efficient Frontier +`add`| add ticker to optimize +`select`| overwrite current tickers with new tickers +`equal`| equally weighted +`property`| weight according to selected info property (e.g. marketCap) +`maxsharpe`| optimizes for maximal Sharpe ratio (a.k.a the tangency portfolio) +`minvol`| optimizes for minimum volatility +`maxquadutil`| maximises the quadratic utility, given some risk aversion +`effret`| maximises return for a given target risk +`effrisk`| minimises risk for a given target return +`ef`| show the efficient frontier   @@ -332,7 +338,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/)   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 f999ed092af..499b6c9cd39 100644 --- a/gamestonk_terminal/comparison_analysis/ca_controller.py +++ b/gamestonk_terminal/comparison_analysis/ca_controller.py @@ -84,8 +84,6 @@ class ComparisonAnalysisController: if self.similar: print(f"[{self.user}] Similar Companies: {', '.join(self.similar)}") - # else: - # print(f"No similar companies [{self.user}]") print("\nComparison Analysis Mode:") print(" help show this comparison analysis menu again") @@ -110,8 +108,9 @@ class ComparisonAnalysisController: print(" performance brief performance comparison") print(" technical brief technical comparison") print("") - print(" > po portfolio optimization for selected tickers") - print("") + if self.similar: + print(" > po portfolio optimization for selected tickers") + print("") return @staticmethod @@ -320,7 +319,7 @@ class ComparisonAnalysisController: finviz_compare_view.screener(other_args, "technical", self.ticker, self.similar) def call_po(self, _): - """Open Portfolio Optimization menu with ticker and similar""" + """Call the portfolio optimization menu with selected tickers""" return po_controller.menu([self.ticker] + self.similar) diff --git a/gamestonk_terminal/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 d1920d1975d..d892d25c1e7 100644 --- a/gamestonk_terminal/discovery/README.md +++ b/gamestonk_terminal/discovery/README.md @@ -32,6 +32,10 @@ This menu aims to discover new stocks, and the usage of the following commands a * performance of sectors, industry, country [Finviz] * [spectrum](#spectrum) * spectrum of sectors, industry, country [Finviz] +* [latest](#latest) + * latest news [Seeking Alpha] +* [trending](#trending) + * trending news [Seeking Alpha] * [ratings](#ratings) * top ratings updates [MarketBeat] @@ -222,6 +226,32 @@ Spectrum of sectors, industry, country. [Source: Finviz] ![cntry_spec](https://user-images.githubusercontent.com/25267873/113639067-48fd5e80-9670-11eb-95cf-0931845ddd12.png) +## latest + +``` +usage: latest [-i N_ID] [-n N_NUM] +``` + +Latest news articles. [Source: Seeking Alpha] +* -i : article number found on Seeking Alpha website +* -n : number of latest articles being printed. Default 10. + +latest + + +## trending + +``` +usage: trending [-i N_ID] [-n N_NUM] +``` + +Trending news articles. [Source: Seeking Alpha] +* -i : article number found on Seeking Alpha website +* -n : number of trending articles being printed. Default 10. + +trending + + ## ratings ``` @@ -234,4 +264,3 @@ Top ratings updates. [Source: MarketBeat] ratings - diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index 6125a377773..2362e8f762b 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -48,6 +48,8 @@ class DiscoveryController: "performance", "spectrum", "ratings", + "latest", + "trending", ] def __init__(self): @@ -89,6 +91,8 @@ class DiscoveryController: print(" valuation valuation of sectors, industry, country [Finviz]") print(" performance performance of sectors, industry, country [Finviz]") print(" spectrum spectrum of sectors, industry, country [Finviz]") + print(" latest latest news [Seeking Alpha]") + print(" trending trending news [Seeking Alpha]") print(" ratings top ratings updates [MarketBeat]") print("") @@ -190,6 +194,14 @@ 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) diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index 59f63e44ae6..b573c506d3f 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,141 @@ 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(num: int) -> List[dict]: + """Returns a list of latest articles + + Parameters + ---------- + pages : 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] + + articles.append( + { + "title": item.text, + "publishedAt": datetime.strptime( + item_row["data-last-date"], "%Y-%m-%d %H:%M:%S %z" + ).strftime("%Y-%m-%d %H:%M:%S"), + "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..a11c548be72 100644 --- a/gamestonk_terminal/discovery/seeking_alpha_view.py +++ b/gamestonk_terminal/discovery/seeking_alpha_view.py @@ -65,3 +65,147 @@ 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 number found on Seeking Alpha website", + ) + parser.add_argument( + "-n", + "--num", + action="store", + dest="n_num", + type=check_positive, + default=10, + help="number of latest articles being printed", + ) + + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-i") + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + # User wants to see all latest news + if ns_parser.n_id == -1: + articles = seeking_alpha_model.get_article_list(ns_parser.n_num) + for idx, article in enumerate(articles): + print( + article["publishedAt"].replace("T", " ").replace("Z", ""), + "-", + article["id"], + "-", + article["title"], + ) + print(article["url"]) + print("") + + if idx >= ns_parser.n_num - 1: + break + + # User wants to access specific article + else: + article = seeking_alpha_model.get_article_data(ns_parser.n_id) + print( + article["publishedAt"][: 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 number found on Seeking Alpha website", + ) + parser.add_argument( + "-n", + "--num", + action="store", + dest="n_num", + type=check_positive, + default=10, + help="number of trending articles being printed", + ) + + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-i") + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + # 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/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/options/README.md b/gamestonk_terminal/options/README.md index a86dceb2b2c..980be7495c4 100644 --- a/gamestonk_terminal/options/README.md +++ b/gamestonk_terminal/options/README.md @@ -1,11 +1,13 @@ # OPTIONS DATA -This menu aims to give insight on options. options can cause significant share price movement, and option pricing gives info about market sentiment for a ticker. +This menu aims to give insight on options. options can cause significant share price movement, and option pricing gives +info about market sentiment for a ticker. * [volume](#volume) * Show traded options for a given expiry [Yahoo Finance] - +* [chains](#chains) + * Display option chains for ## volume @@ -23,3 +25,26 @@ usage: volume --expiry yyyy-mm-dd Display volume graph for a date. [Source: Yahoo Finance] * -e, --expiry : expiration date. + +## chains +Display a table of options chains. + +This data requires a Tradier sandbox developer account, which can be made [here](#https://tradier.com/products/market-data-api) + +```` +usage: chains [--calls] [--puts] [-m MIN] [-M MAX] +```` +* --calls : Flag to only display call options +* --puts : Flag to display only put options +* -m : Minimum strike price to show +* -M : Maximum strike price to show + +For combines tables, call options are on the left and put options are on the right of the strike + +As an example, if I load my ticker and select an expiry, to view the options chain for strikes between $129 and $135, one would use: + +```` +chains -m 129 -M 135 +```` +Which outputs: +![im](https://user-images.githubusercontent.com/18151143/115068041-b191da00-9ebf-11eb-941e-4e255787ab94.png) diff --git a/gamestonk_terminal/options/chains_view.py b/gamestonk_terminal/options/chains_view.py new file mode 100644 index 00000000000..c05552a909e --- /dev/null +++ b/gamestonk_terminal/options/chains_view.py @@ -0,0 +1,208 @@ +"""Helper functions for the options menu""" +__docformat__ = "numpy" + +import argparse +from typing import List +import requests +import pandas as pd +import numpy as np +from tabulate import tabulate +from gamestonk_terminal.config_terminal import TRADIER_TOKEN +from gamestonk_terminal.helper_funcs import ( + check_non_negative, + parse_known_args_and_warn, +) + +option_columns = [ + "bid", + "ask", + "strike", + "bidsize", + "asksize", + "volume", + "open_interest", + "option_type", +] +greek_columns = ["delta", "gamma", "theta", "vega", "ask_iv", "bid_iv", "mid_iv"] +df_columns = option_columns + greek_columns +column_map = {"mid_iv": "iv", "open_interest": "oi", "volume": "vol"} + + +def process_chains(response: requests.models.Response) -> pd.DataFrame: + """ + Function to take in the requests.get and return a DataFrame + Parameters + ---------- + response: requests.models.Response + This is the respnose from tradier api. + + Returns + ------- + opt_chain: pd.DataFrame + Dataframe with all available options + """ + json_response = response.json() + options = json_response["options"]["option"] + + opt_chain = pd.DataFrame(columns=df_columns) + for idx, option in enumerate(options): + data = [option[col] for col in option_columns] + data += [option["greeks"][col] for col in greek_columns] + opt_chain.loc[idx, :] = data + + return opt_chain + + +def get_option_chains(symbol: str, expiry: str) -> pd.DataFrame: + """ + + Parameters + ---------- + symbol : str + Ticker to get options for + expiry : str + Expiration date in the form of "YYYY-MM-DD" + + Returns + ------- + chains: pd.DataFrame + Dataframe with options for the gievn Symbol and Expiration date + """ + params = {"symbol": symbol, "expiration": expiry, "greeks": "true"} + + headers = {"Authorization": f"Bearer {TRADIER_TOKEN}", "Accept": "application/json"} + + response = requests.get( + "https://sandbox.tradier.com/v1/markets/options/chains", + params=params, + headers=headers, + ) + if response.status_code == 401: + print("Error in request.get -- check token") + return + + chains = process_chains(response) + return chains + + +def display_chains(symbol: str, expiry: str, other_args: List[str]): + + parser = argparse.ArgumentParser( + prog="chains", add_help=False, description="Display options chains" + ) + + parser.add_argument( + "--calls", + action="store_true", + default=False, + dest="calls_only", + help="Flag to show calls only", + ) + + parser.add_argument( + "--puts", + action="store_true", + default=False, + dest="puts_only", + help="Flag to show puts only", + ) + parser.add_argument( + "-m", + "--min", + dest="min_sp", + type=check_non_negative, + default=-1, + help="minimum strike price to consider.", + ) + parser.add_argument( + "-M", + "--max", + dest="max_sp", + type=check_non_negative, + default=-1, + help="maximum strike price to consider.", + ) + + chains_df = get_option_chains(symbol, expiry) + + default_columns = [ + "mid_iv", + "delta", + "volume", + "open_interest", + "bid", + "ask", + "strike", + "option_type", + ] + chains_df = chains_df[default_columns].rename(columns=column_map) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + if ns_parser.max_sp == -1 and ns_parser.min_sp == -1: + min_strike = np.percentile(chains_df["strike"], 25) + max_strike = np.percentile(chains_df["strike"], 75) + else: + min_strike = ns_parser.min_sp + max_strike = ns_parser.max_sp + + chains_df = chains_df[chains_df["strike"] >= min_strike] + chains_df = chains_df[chains_df["strike"] <= max_strike] + calls_df = chains_df[chains_df.option_type == "call"].drop( + columns=["option_type"] + ) + puts_df = chains_df[chains_df.option_type == "put"].drop( + columns=["option_type"] + ) + + if ns_parser.calls_only: + print( + tabulate( + calls_df, + headers=calls_df.columns, + tablefmt="grid", + showindex=False, + floatfmt=".2f", + ) + ) + return + + if ns_parser.puts_only: + print( + tabulate( + puts_df, + headers=puts_df.columns, + tablefmt="grid", + showindex=False, + floatfmt=".2f", + ) + ) + return + + puts_df = puts_df[puts_df.columns[::-1]] + chain_table = calls_df.merge(puts_df, on="strike") + + headers = [ + col.strip("_x") + if col.endswith("_x") + else col.strip("_y") + if col.endswith("_y") + else col + for col in chain_table.columns + ] + print( + tabulate( + chain_table, + headers=headers, + tablefmt="fancy_grid", + showindex=False, + floatfmt=".2f", + ) + ) + + except Exception as e: + print(e) + print("") + return diff --git a/gamestonk_terminal/options/op_controller.py b/gamestonk_terminal/options/op_controller.py index e373ba1f677..4caa51dbd63 100644 --- a/gamestonk_terminal/options/op_controller.py +++ b/gamestonk_terminal/options/op_controller.py @@ -9,7 +9,7 @@ import yfinance as yf from prompt_toolkit.completion import NestedCompleter from gamestonk_terminal.helper_funcs import get_flair, parse_known_args_and_warn from gamestonk_terminal import feature_flags as gtff -from gamestonk_terminal.options import volume_view +from gamestonk_terminal.options import volume_view, chains_view from gamestonk_terminal.menu import session @@ -17,7 +17,7 @@ class OptionsController: """Options Controller class.""" # Command choices - CHOICES = ["help", "q", "quit", "exp", "voi", "vcalls", "vputs"] + CHOICES = ["help", "q", "quit", "exp", "voi", "vcalls", "vputs", "chains"] def __init__(self, ticker: str, last_adj_close_price: float): """Construct data.""" @@ -93,6 +93,8 @@ class OptionsController: print(" vcalls calls volume + open interest plot") print(" vputs puts volume + open interest plot") print("") + print(" chains display option chains") + print("") return def switch(self, an_input: str): @@ -158,6 +160,9 @@ class OptionsController: self.options.puts, ) + def call_chains(self, other_args): + chains_view.display_chains(self.ticker, self.expiry_date, other_args) + def menu(ticker: str, last_adj_close_price: float): """ Options Menu. """ diff --git a/gamestonk_terminal/options/volume_view.py b/gamestonk_terminal/options/volume_view.py index a20e4cafc54..a4f3062a960 100644 --- a/gamestonk_terminal/options/volume_view.py +++ b/gamestonk_terminal/options/volume_view.py @@ -2,12 +2,12 @@ __docformat__ = "numpy" from bisect import bisect_left -import seaborn as sns from typing import List import argparse import pandas as pd import matplotlib.pyplot as plt import numpy as np +import seaborn as sns from gamestonk_terminal.helper_funcs import ( plot_autoscale, diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md index bf96cf77629..3e9e44d4603 100644 --- a/gamestonk_terminal/portfolio_optimization/README.md +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -1,169 +1,224 @@ # Portfolio Optimization -This menu aims to discover optimal portfolios for selected stocks. +This menu aims to optimize a portfolio of pre-loaded stocks, and the usage of the following commands along with an example will be exploited below. -Currently we support two methods: +* [add](#add) + * add ticker to optimize +* [select](#select) + * overwrite current tickers with new tickers +* [equal](#equal) + * equally weighted +* [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 -* [Property Weighting](#weighting) - * Equal Weights - * Market Cap Weighting - * Divident Yield Weighting -* [Mean-Variance-Optimization](#eff_front) - * Max sharpe ratio - * Minimum volatility - * Maximum returns at given risk level - * Minimum risk level at a given return +[Mean Variance Optimization](#Mean_Variance_Optimization) -## Procedure -There are three ways to load stocks to be analyzed. -* add -* select -* from ca menu -###add -Adds selected tickers to the menu to be considered +* [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 -```` -usage: add ticker1,ticker2,ticker3,... -```` -###select -Clears current list and loads selected ticker -```` -usage: select ticker1,ticker2,ticker3,... -```` -### ca menu -From the ca menu, the loaded ticker and the selected similar tickers can be loaded by entering the `> po` menu. +### add -From this `po` menu, you can return to `ca` using `ca`, which will keep your loaded stock, but reset the selected stocks from that menu. +```text +add [-t ADD_TICKERS] +``` -Once your stocks are listed, you can select one of the options. -## Property weighted -* equal weights -* market cap weighted -* dividend yield weighted -### equal weights -Returns an equally weighted portfolio where the weights are 1/number of stocks. -```` -equal_weight [-v --value VALUE] [--pie] -```` -* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. -* --pie Flag that displays a pie chart of the allocations. +Add/Select tickers for portfolio to be optimized. -### market cap weighted -Returns portfolio values that are weighted by their relative Market Cap. -```` -mkt_cap [-v --value VALUE] [--pie] -```` -* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. -* --pie Flag that displays a pie chart of the allocations. +* -t : Tickers to be used in the portfolio to optimize +* -###dividend yield weighted -Returns portfolio values that are weighted by relative Dividend Yield. -```` -div_yield [-v --value VALUE] [--pie] -```` -* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. -* --pie Flag that displays a pie chart of the allocations. +### select -## Mean Variance Optimization +```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. - +

+ +

-With constraints: +* Where S is the covariance matrix between stocks and R is the expected returns. +* The condition that all weights add up to 1 just implies that you want to have a net long portfolio (with no margin). +* A long-short portfolio can have negative weights and usually wants to have everything add up to 0 for a market-neutral strategy. - +Currently, we do not allow for changing risk models or adding constraints. +If there is something specific, please submit a feature request, or if you can write it, feel free to add a PR! +All of these commands use [PyPortFolioOpt](#https://pyportfolioopt.readthedocs.io/en/latest/index.html) package. -\ - -Where S is the covariance matrix between stocks and R is the expected returns. The condition that all weights add up to 1 -just implies that you want to have a net long portfolio (with no margin). -A long-short portfolio can have negative weights and usually wants to have everything add up to 0 for a market-neutral strategy. +### maxsharpe -Currently, we do not allow for changing risk models or adding constraints. If there is something specific, please submit a feature request, or if you can -write it, feel free to add a PR! - -All of our current implementations use the [PyPortFolioOpt](#https://pyportfolioopt.readthedocs.io/en/latest/index.html) package. - -### max_sharpe -The sharpe ratio is defined as - -(Mean Returns - Risk Free Rate)/(Standard Deviation of Returns) - -The usage is: ```` -max_sharpe [-p PERIOD] [-v --value VALUE] [--pie] +usage: max_sharpe [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-v VALUE] [--pie] [-r RISK_FREE_RATE] +```` + +Maximise the Sharpe Ratio. The result is also referred to as the tangency portfolio, as it is the portfolio for which the capital market line is tangent to the efficient frontier. This is a convex optimization problem after making a certain variable substitution. See Cornuejols and Tutuncu (2006) for more. The sharpe ratio is defined as (Mean Returns - Risk Free Rate)/(Standard Deviation of Returns). + +* -p : Amount of time to retrieve data from yfinance. Default: 3mo. +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights + +maxsharpe + + +### minvol + +```` +usage: minvol [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-v VALUE] [--pie] ```` -* -p/--period Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo. -* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. -* --pie Flag that displays a pie chart of the allocations. -### min_vol This portfolio minimizes the total volatility, which also means it has the smallest returns among the efficient frontier. -The usage is: -```` -min_vol [-p PERIOD] [-v --value VALUE] [--pie] -```` -* -p/--period Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo. -* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. -* --pie Flag that displays a pie chart of the allocations. -### eff_risk -This portfolio maximizes the returns at a given risk tolerance -The usage is: -```` -eff_risk [-p PERIOD] [-r --risk RISK_LEVEL] [-v --value VALUE] [--pie] -```` -* -p/--period Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo. -* -r/--risk Risk tolerance. Default is 0.1 (10%) -* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. -* --pie Flag that displays a pie chart of the allocations. +* -p : Amount of time to retrieve data from yfinance. Default: 3mo. +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights -### eff_ret -This portfolio minimizes the risk at a given return level -The usage is: -```` -eff_ret [-p PERIOD] [-r --return] [-v --value VALUE] [--pie] -```` -* -p/--period Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo. -* -r/--return. Desired return. Default is 0.1 (10%) -* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. -* --pie Flag that displays a pie chart of the allocations. +![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] +```` -### show_eff This function plots random portfolios based on their risk and returns and shows the efficient frontier. -The usage is: -```` -show_eff [-p PERIOD] [-n N_PORTFOLIOS] -```` -* -p/--period Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo. -* -n Number of portfolios to simulate. + +* -n : Number of portfolios to simulate. Default 300. +* -p : Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo. -##Sample Usage -In this example, we generate weights for a list of 6 stocks using the eff_ret command. This optimization looks to maximize returns -at a given risk level. We start by adding the stocks we want to analyze: -```` -select aapl,amzn,msft,f,gm,ge -```` -Which shows: -```` -Current Tickers: GE, GM, AMZN, AAPL, F, MSFT -```` -To perform the optimization, we will set a target return of 25% (.25). Given how stocks performed during COVID, -there is a lot of volatility, so many optimizations may not get low volatility. The module also returns annualized volatility, -so the number is your portfolio volatility * `sqrt(252`. This optimization will be (including a pie chart!). Note we could also supply a different -time period, which changes the expected returns and historical volatility, which changes the optimization. We could also specify a dollar -amount that you wish to allocate using the `-v` flag. -```` -eff_ret -r .25 --pie -```` -The console will show (numbers will vary based on when this is done). -![console](https://user-images.githubusercontent.com/18151143/114740311-bd429c80-9d17-11eb-90e2-97430781431a.png) - -And the pie chart: - -![yummypie](https://user-images.githubusercontent.com/18151143/114740289-b9167f00-9d17-11eb-9c29-470785b21d09.png) - -Note that since `AAPL` had zero allocation, it was omitted from the chart. \ No newline at end of file +![ef](https://user-images.githubusercontent.com/25267873/114958281-93de5980-9e5a-11eb-967b-d37ddd4f3e73.png) diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py index 3ca81f396a2..f88ad80463c 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py @@ -13,21 +13,113 @@ from pypfopt import risk_models from pypfopt import expected_returns from gamestonk_terminal.config_plot import PLOT_DPI from gamestonk_terminal import feature_flags as gtff -from gamestonk_terminal.helper_funcs import plot_autoscale, parse_known_args_and_warn +from gamestonk_terminal.helper_funcs import plot_autoscale -title_maps = { - "max_sharpe": "Maximum Sharpe Portfolio", - "min_volatility": "Minimum Volatility Portfolio", - "eff_risk": "Maximum Return Portfolio at Risk = {:1f} %", - "eff_ret": "Minimum Volatility Portfolio at Target Return = {:.1f} %", - "equal": "Equally Weighted Portfolio", - "marketCap": "MarketCap Weighted Portfolio", - "dividendYield": "Dividend Yield Weighted Portfolio", -} +l_valid_property_infos = [ + "previousClose", + "regularMarketOpen", + "twoHundredDayAverage", + "trailingAnnualDividendYield", + "payoutRatio", + "volume24Hr", + "regularMarketDayHigh", + "navPrice", + "averageDailyVolume10Day", + "totalAssets", + "regularMarketPreviousClose", + "fiftyDayAverage", + "trailingAnnualDividendRate", + "open", + "toCurrency", + "averageVolume10days", + "expireDate", + "yield", + "algorithm", + "dividendRate", + "exDividendDate", + "beta", + "circulatingSupply", + "regularMarketDayLow", + "priceHint", + "currency", + "trailingPE", + "regularMarketVolume", + "lastMarket", + "maxSupply", + "openInterest", + "marketCap", + "volumeAllCurrencies", + "strikePrice", + "averageVolume", + "priceToSalesTrailing12Months", + "dayLow", + "ask", + "ytdReturn", + "askSize", + "volume", + "fiftyTwoWeekHigh", + "forwardPE", + "fromCurrency", + "fiveYearAvgDividendYield", + "fiftyTwoWeekLow", + "bid", + "dividendYield", + "bidSize", + "dayHigh", + "annualHoldingsTurnover", + "enterpriseToRevenue", + "beta3Year", + "profitMargins", + "enterpriseToEbitda", + "52WeekChange", + "morningStarRiskRating", + "forwardEps", + "revenueQuarterlyGrowth", + "sharesOutstanding", + "fundInceptionDate", + "annualReportExpenseRatio", + "bookValue", + "sharesShort", + "sharesPercentSharesOut", + "fundFamily", + "lastFiscalYearEnd", + "heldPercentInstitutions", + "netIncomeToCommon", + "trailingEps", + "lastDividendValue", + "SandP52WeekChange", + "priceToBook", + "heldPercentInsiders", + "shortRatio", + "sharesShortPreviousMonthDate", + "floatShares", + "enterpriseValue", + "threeYearAverageReturn", + "lastSplitFactor", + "legalType", + "lastDividendDate", + "morningStarOverallRating", + "earningsQuarterlyGrowth", + "pegRatio", + "lastCapGain", + "shortPercentOfFloat", + "sharesShortPriorMonth", + "impliedSharesOutstanding", + "fiveYearAverageReturn", + "regularMarketPrice", +] + + +def check_valid_property_type(property: str) -> str: + """ Check that the property selected is valid """ + if property in l_valid_property_infos: + return property + else: + raise argparse.ArgumentTypeError(f"{property} is not a valid info") def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFrame: - """ + """Get adjusted closing price for each stock in the list Parameters ---------- @@ -51,8 +143,8 @@ def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFra def prepare_efficient_frontier(stock_prices: pd.DataFrame): - """ - Take in a dataframe of prices and return an efficient frontier object + """Take in a dataframe of prices and return an efficient frontier object + Parameters ---------- stock_prices : DataFrame @@ -82,11 +174,11 @@ def display_weights(weights: dict): weight_df = pd.DataFrame.from_dict(data=weights, orient="index", columns=["value"]) if math.isclose(weight_df.sum()["value"], 1, rel_tol=0.1): weight_df["weight"] = (weight_df["value"] * 100).astype(str).apply( - lambda s: s[:6] + lambda s: " " + s[:4] if s.find(".") == 1 else "" + s[:5] ) + " %" - print(pd.DataFrame(weight_df["weight"])) + print(pd.DataFrame(weight_df["weight"]).to_string(header=False)) else: - print(weight_df) + print(weight_df.to_string(header=False)) def my_autopct(x): @@ -97,17 +189,16 @@ def my_autopct(x): return "" -def pie_chart_weights(weights: dict, optimizer: str, value: float): - """ - Show a pie chart of holdings +def pie_chart_weights(weights: dict, title_opt: str): + """Show a pie chart of holdings + Parameters ---------- weights: dict - weights to display. Keys are stocks. Values are either weights or values if -v specified - optimzer: str - Optimization technique used for title + Weights to display, where keys are tickers, and values are either weights or values if -v specified + title: str + Title to be used on the plot title """ - plt.close("all") if not weights: return @@ -122,31 +213,28 @@ def pie_chart_weights(weights: dict, optimizer: str, value: float): total_size = np.sum(sizes) - leg_labels = [ - f"{str(a)}: {str(round(100*b/total_size,3))[:4]}%" - for a, b in zip(stocks, sizes) - ] plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI) + if math.isclose(sum(sizes), 1, rel_tol=0.1): wedges, _, autotexts = plt.pie( sizes, labels=stocks, autopct=my_autopct, textprops=dict(color="k"), - explode=[s / (5 * total_size) for s in sizes], + wedgeprops={"linewidth": 3.0, "edgecolor": "white"}, normalize=True, - shadow=True, ) + plt.setp(autotexts, color="white", fontweight="bold") else: wedges, _, autotexts = plt.pie( sizes, labels=stocks, autopct="", textprops=dict(color="k"), - explode=[s / (5 * total_size) for s in sizes], + wedgeprops={"linewidth": 3.0, "edgecolor": "white"}, normalize=True, - shadow=True, ) + plt.setp(autotexts, color="white", fontweight="bold") for i, a in enumerate(autotexts): if sizes[i] / total_size > 0.05: a.set_text(f"{sizes[i]:.2f}") @@ -155,72 +243,37 @@ def pie_chart_weights(weights: dict, optimizer: str, value: float): plt.axis("equal") - plt.legend( + leg1 = plt.legend( wedges, - leg_labels, - title="Stocks", - loc="center left", - bbox_to_anchor=(0.85, 0, 0.5, 1), + [str(s) for s in stocks], + title=" Ticker", + loc="upper left", + bbox_to_anchor=(0.80, 0, 0.5, 1), + frameon=False, ) + leg2 = plt.legend( + wedges, + [ + f"{' ' if ((100*s/total_size) < 10) else ''}{100*s/total_size:.2f}%" + for s in sizes + ], + title=" ", + loc="upper left", + handlelength=0, + bbox_to_anchor=(0.91, 0, 0.5, 1), + frameon=False, + ) + plt.gca().add_artist(leg1) + plt.gca().add_artist(leg2) plt.setp(autotexts, size=8, weight="bold") - if optimizer in ["eff_ret", "eff_risk"]: - plt.title(title_maps[optimizer].format(100 * value)) - else: - plt.title(title_maps[optimizer]) + plt.gca().set_title(title_opt, pad=20) if gtff.USE_ION: plt.ion() + plt.tight_layout() + plt.show() print("") - - -def parse_from_port_type( - parser_in: argparse.ArgumentParser, port_type: str, other_args: List[str] -): - """ - - Parameters - ---------- - parser_in: ArgumentParser - parser to get data from - port_type: str - Type of optimization that will be done. One of ["max_sharpe","min_vol", "eff_risk", "eff_ret"] - other_args: List[str] - Arguments passed to function - Returns - ------- - - ns_parser: - Parsed arguments - """ - - if port_type in ["max_sharpe", "min_volatility"]: - ns_parser = parse_known_args_and_warn(parser_in, other_args) - if not ns_parser: - return None - return ns_parser - - elif port_type == "eff_risk": - parser_in.add_argument( - "-r", "--risk", type=float, dest="risk_level", default=0.1 - ) - ns_parser = parse_known_args_and_warn(parser_in, other_args) - if not ns_parser: - return None - return ns_parser - - elif port_type == "eff_ret": - parser_in.add_argument( - "-r", "--return", type=float, dest="target_return", default=0.1 - ) - - ns_parser = parse_known_args_and_warn(parser_in, other_args) - if not ns_parser: - return None - return ns_parser - else: - print("Incorrect portfolio optimizer type\n") - return None diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py index e9d74a76138..6c61666d5bf 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -10,13 +10,17 @@ from pypfopt import plotting from pypfopt import risk_models from pypfopt import expected_returns from pypfopt import EfficientFrontier -from gamestonk_terminal.helper_funcs import parse_known_args_and_warn, plot_autoscale +from gamestonk_terminal.helper_funcs import ( + parse_known_args_and_warn, + plot_autoscale, + check_non_negative, +) from gamestonk_terminal.portfolio_optimization.optimizer_helper import ( process_stocks, prepare_efficient_frontier, pie_chart_weights, display_weights, - parse_from_port_type, + check_valid_property_type, ) from gamestonk_terminal.config_plot import PLOT_DPI from gamestonk_terminal import feature_flags as gtff @@ -35,6 +39,20 @@ period_choices = [ "max", ] +d_period = { + "1d": "[1 Day]", + "5d": "[5 Days]", + "1mo": "[1 Month]", + "3mo": "[3 Months]", + "6mo": "[6 Months]", + "1y": "[1 Year]", + "2y": "[2 Years]", + "5y": "[5 Years]", + "10y": "[10 Years]", + "ytd": "[Year-to-Date]", + "max": "[All-time]", +} + def equal_weight(stocks: List[str], other_args: List[str]): """ @@ -53,8 +71,8 @@ def equal_weight(stocks: List[str], other_args: List[str]): """ parser = argparse.ArgumentParser( add_help=False, - prog="equal_weight", - description="Return equally weighted portfolio holdings", + prog="equal", + description="Returns an equally weighted portfolio", ) parser.add_argument( @@ -76,14 +94,17 @@ def equal_weight(stocks: List[str], other_args: List[str]): ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return + + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return + values = {} - n_stocks = len(stocks) for stock in stocks: - values[stock] = ns_parser.value * round(1 / n_stocks, 5) + values[stock] = ns_parser.value * round(1 / len(stocks), 5) if ns_parser.pie: - pie_chart_weights(values, "equal", 0) - if n_stocks >= 1: - print("Equal Weight Portfolio: ") + pie_chart_weights(values, "Equally Weighted Portfolio") + else: display_weights(values) print("") @@ -92,16 +113,15 @@ def equal_weight(stocks: List[str], other_args: List[str]): print("") -def property_weighting(stocks: List[str], property_type: str, other_args: List[str]): - """ - Property weighted portfolio where each weight is the relative fraction. +def property_weighting(stocks: List[str], other_args: List[str]): + """Weighted portfolio where each weight is the relative fraction of a specified info property. + Parameters ---------- stocks: List[str] List of tickers to be included in optimization - property_type: str - Property to weight by. Can be anything in yfinance.Ticker().info. Examples: - "marketCap", "dividendYield", etc + other_args : List[str] + Command line arguments to be processed with argparse Returns ------- @@ -110,10 +130,32 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s """ parser = argparse.ArgumentParser( add_help=False, - prog="market_cap_weighted", - description="Return portfolio weights/values that are weighted by marketcap", + prog="property", + description="Returns a portfolio that is weighted based on a selected property info", + ) + parser.add_argument( + "-p", + "--property", + required=True 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", @@ -129,33 +171,42 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s default=False, help="Display a pie chart for weights", ) - weights = {} - prop = {} - prop_sum = 0 try: + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-p") + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return + + weights = {} + prop = {} + prop_sum = 0 for stock in stocks: - stock_prop = yf.Ticker(stock).info[property_type] + stock_prop = yf.Ticker(stock).info[ns_parser.property] if stock_prop is None: stock_prop = 0 prop[stock] = stock_prop 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, property_type, 0) - - if property_type == "marketCap": - print("Market Cap Weighted Portfolio: ") - - elif property_type == "dividendYield": - print("Dividend Yield Weighted Portfolio: ") - - if len(stocks) >= 1: + pie_chart_weights( + weights, "Weighted Portfolio based on " + ns_parser.property + ) + else: display_weights(weights) print("") @@ -165,23 +216,23 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s return -def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): +def max_sharpe(stocks: List[str], other_args: List[str]): """ - Return a portfolio based on condition in port_type Defaults to 3m of historical data + Return a portfolio that maximises the Sharpe Ratio + Parameters ---------- stocks: List[str] List of the stocks to be included in the weights - port_type: str - Method to be used on ef object (example: max_sharpe, min_volatility) + Returns ------- weights: dict Dictionary of weights where keys are the tickers. """ - - parser = argparse.ArgumentParser(add_help=False, prog=port_type) - + parser = argparse.ArgumentParser( + add_help=False, prog="maxsharpe", description="Maximise the Sharpe Ratio" + ) parser.add_argument( "-p", "--period", @@ -190,7 +241,100 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): help="period to get yfinance data from", choices=period_choices, ) + parser.add_argument( + "-v", + "--value", + 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", @@ -208,67 +352,363 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): ) try: - ns_parser = parse_from_port_type(parser, port_type, other_args) + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: - return + return None + if len(stocks) < 2: print("Please have at least 2 loaded tickers to calculate weights.\n") return + period = ns_parser.period stock_prices = process_stocks(stocks, period) ef = prepare_efficient_frontier(stock_prices) - if port_type == "max_sharpe": + sp = d_period[ns_parser.period] - ef_sharpe = dict(ef.max_sharpe()) - weights = { - key: ns_parser.value * round(value, 5) - for key, value in ef_sharpe.items() - } - val = 0 - print("Weights that maximize Sharpe Ratio:") + ef_opt = dict(ef.min_volatility()) + s_title = f"{sp} Weights that minimize volatility" - elif port_type == "min_volatility": - - ef_min_vol = dict(ef.min_volatility()) - weights = { - key: ns_parser.value * round(value, 5) - for key, value in ef_min_vol.items() - } - val = 0 - print("Weights that minimize volatility") - - elif port_type == "eff_risk": - - ef_eff_risk = dict(ef.efficient_risk(ns_parser.risk_level)) - weights = { - key: ns_parser.value * round(value, 5) - for key, value in ef_eff_risk.items() - } - val = ns_parser.risk_level - print(f"Weights for maximizing returns at risk = {100*val:.1f} %") - - elif port_type == "eff_ret": - - ef_eff_risk = dict(ef.efficient_return(ns_parser.target_return)) - weights = { - key: ns_parser.value * round(value, 5) - for key, value in ef_eff_risk.items() - } - val = ns_parser.target_return - print(f"Weights for minimizing risk at target return = {100*val:.1f} %") - - else: - raise ValueError("EF Method not found") + weights = { + key: ns_parser.value * round(value, 5) for key, value in ef_opt.items() + } if ns_parser.pie: - pie_chart_weights(weights, port_type, val) + 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) @@ -277,7 +717,12 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): def show_ef(stocks: List[str], other_args: List[str]): - parser = argparse.ArgumentParser(add_help=False, prog="ef") + parser = argparse.ArgumentParser( + add_help=False, + prog="ef", + description="""This function plots random portfolios based + on their risk and returns and shows the efficient frontier.""", + ) parser.add_argument( "-p", @@ -288,10 +733,18 @@ def show_ef(stocks: List[str], other_args: List[str]): choices=period_choices, ) parser.add_argument( - "-n", default=300, dest="n_port", help="number of portfolios to simulate" + "-n", + "--number-portfolios", + default=300, + type=check_non_negative, + dest="n_port", + help="number of portfolios to simulate", ) try: + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-n") ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return @@ -319,7 +772,7 @@ def show_ef(stocks: List[str], other_args: List[str]): ret_sharpe, std_sharpe, _ = ef.portfolio_performance() ax.scatter(std_sharpe, ret_sharpe, marker="*", s=100, c="r", label="Max Sharpe") - ax.set_title("Efficient Frontier") + ax.set_title(f"Efficient Frontier simulating {ns_parser.n_port} portfolios") ax.legend() plt.tight_layout() plt.grid(b=True, which="major", color="#666666", linestyle="-") diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index df2f8a55fa6..c66ca63e846 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -9,7 +9,7 @@ from prompt_toolkit.completion import NestedCompleter from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import get_flair, parse_known_args_and_warn from gamestonk_terminal.menu import session -from gamestonk_terminal.portfolio_optimization import optimizer_view as po_api +from gamestonk_terminal.portfolio_optimization import optimizer_view class PortfolioOptimization: @@ -20,14 +20,17 @@ class PortfolioOptimization: "quit", "select", "add", - "equal_weight", - "mkt_cap", - "div_yield", - "max_sharpe", - "min_vol", - "eff_risk", - "eff_ret", - "show_ef", + "equal", + "mktcap", + "dividend", + "property", + "maxsharpe", + "minvol", + "effret", + "effrisk", + "maxquadutil", + "ef", + "ca", "yolo", ] @@ -52,21 +55,33 @@ class PortfolioOptimization: print(" quit quit to abandon program") print(f"\nCurrent Tickers: {('None', ', '.join(tickers))[bool(tickers)]}") print("") - print(" add add ticker to optimize") - print(" select overwrite current tickers with new tickers") + print(" add add ticker to optimize") + print(" select overwrite current tickers with new tickers") print("") print("Optimization:") + print(" equal equally weighted") + print(" mktcap 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(" equal_weight equally weighted portfolio") - print(" mkt_cap marketcap weighted portfolio") - print(" div_yield dividend weighted portfolio") - print(" max_sharpe portfolio with maximum sharpe ratio") - print(" min_vol portfolio with minimum volatility") - print(" eff_risk portfolio that maximizes returns at given risk") - print(" eff_ret portfolio that minimizes risk at given return") - print(" show_ef show the efficient frontier") + print("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("") - plt.close("all") + 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 @@ -97,36 +112,55 @@ class PortfolioOptimization: return True def call_add(self, other_args: List[str]): + """Process add command""" self.add_stocks(self, other_args) def call_select(self, other_args: List[str]): + """Process select command""" self.tickers = [] self.add_stocks(self, other_args) - def call_equal_weight(self, other_args: List[str]): - po_api.equal_weight(self.tickers, other_args) + def call_equal(self, other_args: List[str]): + """Process equal command""" + optimizer_view.equal_weight(self.tickers, other_args) - def call_mkt_cap(self, other_args: List[str]): - po_api.property_weighting(self.tickers, "marketCap", other_args) + def call_mktcap(self, other_args: List[str]): + """Process mktcap command""" + other_args.insert(0, "marketCap") + optimizer_view.property_weighting(self.tickers, other_args) - def call_div_yield(self, other_args: List[str]): - po_api.property_weighting(self.tickers, "dividendYield", other_args) + def call_dividend(self, other_args: List[str]): + """Process dividend command""" + other_args.insert(0, "dividendYield") + optimizer_view.property_weighting(self.tickers, other_args) - def call_max_sharpe(self, other_args: List[str]): - po_api.ef_portfolio(self.tickers, "max_sharpe", other_args) + def call_property(self, other_args: List[str]): + """Process property command""" + optimizer_view.property_weighting(self.tickers, other_args) - def call_min_vol(self, other_args: List[str]): - po_api.ef_portfolio(self.tickers, "min_volatility", other_args) + def call_maxsharpe(self, other_args: List[str]): + """Process maxsharpe command""" + optimizer_view.max_sharpe(self.tickers, other_args) - def call_eff_risk(self, other_args: List[str]): - po_api.ef_portfolio(self.tickers, "eff_risk", other_args) + def call_minvol(self, other_args: List[str]): + """Process minvol command""" + optimizer_view.min_volatility(self.tickers, other_args) - def call_eff_ret(self, other_args: List[str]): - po_api.ef_portfolio(self.tickers, "eff_ret", other_args) + def call_maxquadutil(self, other_args: List[str]): + """Process maxquadutil command""" + optimizer_view.max_quadratic_utility(self.tickers, other_args) - def call_show_ef(self, other_args): - po_api.show_ef(self.tickers, other_args) - print("") + 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 :) @@ -136,11 +170,11 @@ class PortfolioOptimization: @staticmethod def add_stocks(self, other_args: List[str]): - """ Add ticker to current list for optimization""" + """ Add ticker or Select tickes for portfolio to be optimized """ parser = argparse.ArgumentParser( add_help=False, - prog="add", - description="""Add tickers for optimizing.""", + prog="add/select", + description="""Add/Select tickers for portfolio to be optimized.""", ) parser.add_argument( "-t", @@ -148,10 +182,9 @@ class PortfolioOptimization: dest="add_tickers", type=lambda s: [str(item).upper() for item in s.split(",")], default=[], - help="add tickers to optimzation.", + help="tickers to be used in the portfolio to optimize.", ) try: - if other_args: if "-" not in other_args[0]: other_args.insert(0, "-t") @@ -162,12 +195,14 @@ class PortfolioOptimization: tickers = set(self.tickers) for ticker in ns_parser.add_tickers: tickers.add(ticker) + + if self.tickers: + print( + f"\nCurrent Tickers: {('None', ', '.join(tickers))[bool(tickers)]}" + ) + self.tickers = list(tickers) - print( - f"\nCurrent Tickers: {('None', ', '.join(self.tickers))[bool(self.tickers)]}" - ) - print("") except Exception as e: print(e) @@ -196,6 +231,8 @@ def menu(tickers: List[str]): an_input = input(f"{get_flair()} (po)> ") try: + plt.close("all") + process_input = po_controller.switch(an_input) if process_input is not None: diff --git a/gamestonk_terminal/screener/README.md b/gamestonk_terminal/screener/README.md index 6271a7684db..3135fb3f981 100644 --- a/gamestonk_terminal/screener/README.md +++ b/gamestonk_terminal/screener/README.md @@ -23,8 +23,8 @@ This menu aims to filter stocks based on pre-specified preset filters, and the u * contains [Oversold (-s) signal example](#signal-oversold) * [signals](#signals) * view filter signals (e.g. -s top_gainers) [Finviz] -* [po](#port_opt) - * go to the portfolio optimization menu +* [> po](portfolio_optimization/README.md) + * portfolio optimization for last screened tickers ## view @@ -190,4 +190,4 @@ usage: po ticker1 ticker2 ... ```` example: po aapl msft tsla gme -```` \ No newline at end of file +```` diff --git a/gamestonk_terminal/screener/finviz_view.py b/gamestonk_terminal/screener/finviz_view.py index 4ca3bb96857..3c331be9649 100644 --- a/gamestonk_terminal/screener/finviz_view.py +++ b/gamestonk_terminal/screener/finviz_view.py @@ -214,12 +214,16 @@ def screener(other_args: List[str], loaded_preset: str, data_type: str): if isinstance(df_screen, pd.DataFrame): print(df_screen.to_string()) - print("") + print("") + return list(df_screen["Ticker"].values) + else: + print("") + return [] except Exception as e: print(e) print("") - return + return [] def view_signals(other_args: List[str]): diff --git a/gamestonk_terminal/screener/screener_controller.py b/gamestonk_terminal/screener/screener_controller.py index 0fe364750b0..7edc438c501 100644 --- a/gamestonk_terminal/screener/screener_controller.py +++ b/gamestonk_terminal/screener/screener_controller.py @@ -41,6 +41,7 @@ class ScreenerController: def __init__(self): """Constructor""" self.preset = "template" + self.screen_tickers = [] self.scr_parser = argparse.ArgumentParser(add_help=False, prog="scr") self.scr_parser.add_argument( "cmd", @@ -71,8 +72,11 @@ class ScreenerController: print("") print(" signals view filter signals (e.g. -s top_gainers)") print("") - print(" > po go to the portfolio optimization menu") - print("") + if self.screen_tickers: + print(f"Last screened tickers: {', '.join(self.screen_tickers)}") + print("") + print(" > po portfolio optimization for last screened tickers") + print("") @staticmethod def view_available_presets(other_args: List[str]): @@ -191,39 +195,41 @@ class ScreenerController: def call_historical(self, other_args: List[str]): """Process historical command""" - yahoo_finance_view.historical(other_args, self.preset) + self.screen_tickers = yahoo_finance_view.historical(other_args, self.preset) def call_overview(self, other_args: List[str]): """Process overview command""" - finviz_view.screener(other_args, self.preset, "overview") + self.screen_tickers = finviz_view.screener(other_args, self.preset, "overview") def call_valuation(self, other_args: List[str]): """Process valuation command""" - finviz_view.screener(other_args, self.preset, "valuation") + self.screen_tickers = finviz_view.screener(other_args, self.preset, "valuation") def call_financial(self, other_args: List[str]): """Process financial command""" - finviz_view.screener(other_args, self.preset, "financial") + self.screen_tickers = finviz_view.screener(other_args, self.preset, "financial") def call_ownership(self, other_args: List[str]): """Process ownership command""" - finviz_view.screener(other_args, self.preset, "ownership") + self.screen_tickers = finviz_view.screener(other_args, self.preset, "ownership") def call_performance(self, other_args: List[str]): """Process performance command""" - finviz_view.screener(other_args, self.preset, "performance") + self.screen_tickers = finviz_view.screener( + other_args, self.preset, "performance" + ) def call_technical(self, other_args: List[str]): """Process technical command""" - finviz_view.screener(other_args, self.preset, "technical") + self.screen_tickers = finviz_view.screener(other_args, self.preset, "technical") def call_signals(self, other_args: List[str]): """Process signals command""" finviz_view.view_signals(other_args) def call_po(self, other_args: List[str]): - """Call the portfolio optimization menu""" - return po_controller.menu(other_args) + """Call the portfolio optimization menu with selected tickers""" + return po_controller.menu(self.screen_tickers) def menu(): diff --git a/gamestonk_terminal/screener/yahoo_finance_view.py b/gamestonk_terminal/screener/yahoo_finance_view.py index 6ad1c6e93bb..2a05ddbeec1 100644 --- a/gamestonk_terminal/screener/yahoo_finance_view.py +++ b/gamestonk_terminal/screener/yahoo_finance_view.py @@ -1,5 +1,6 @@ import argparse from typing import List +import random from pandas.plotting import register_matplotlib_converters import matplotlib.pyplot as plt import datetime @@ -119,10 +120,12 @@ def historical(other_args: List[str], preset_loaded: str): if len(l_stocks) > 10: print( - "We limit stocks shown to 10, as after this the plot becomes too noisy. " - "Also, we ran out of different colors." + "\nThe limit of stocks to compare with are 10. Hence, 10 random similar stocks will be displayed.", + "\nThe selected list will be:", ) - l_stocks = l_stocks[:10] + random.shuffle(l_stocks) + l_stocks = sorted(l_stocks[:10]) + print(", ".join(l_stocks)) while l_stocks: l_parsed_stocks = list() @@ -179,8 +182,11 @@ def historical(other_args: List[str], preset_loaded: str): plt.show() print("") + return l_parsed_stocks except SystemExit: print("Similar companies need to be provided", "\n") + return [] except Exception as e: print(e, "\n") + return [] 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 ```