+
+
+## 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.
+
+
+
+
+## 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.
+
+
+
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.
+
+## dp
+
+```text
+usage: dp
+```
+
+Display barchart of dark pool (ATS) and OTC (Non ATS) data
+
+
+
+## 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
+
+
+## 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.
+
+
+
+## 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.
+
+
+
+
+## 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.
+
+
+
+
+## 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.
+
+
+
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
+
+
+
+
+### 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
+
+
+
+
+### 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
+
+
+
+
+### 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
+
+
+
+
+## Mean Variance Optimization
+
+These approaches are based off of the efficient frontier approach, which is meant to solve the following optimization problem.
+
+
+
+
+
+
+### 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
+
+
+
+
+
+### maxquadutil
+
+````
+usage: maxquadutil [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-v VALUE] [--pie] [-r RISK_AVERSION] [-n]
+````
+
+Maximises the quadratic utility, given some risk aversion.
+
+* -p : Amount of time to retrieve data from yfinance. Default: 3mo.
+* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights.
+* --pie : Display a pie chart for weights
+* -r : Risk aversion parameter
+* -n : Whether the portfolio should be market neutral (weights sum to zero). Defaults: False.
+
+
+
+
+### effret
+
+````
+usage: effret [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-v VALUE] [--pie] [-t TARGET_RETURN] [-n]
+````
+
+Calculate the 'Markowitz portfolio', minimising volatility for a given target return.
+
+* -p : Amount of time to retrieve data from yfinance. Default: 3mo.
+* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights.
+* --pie : Display a pie chart for weights
+* -t : The desired return of the resulting portfolio
+* -n : Whether the portfolio should be market neutral (weights sum to zero). Defaults: False.
+
+
+
+
+### effrisk
+
+````
+usage: effrisk [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-v VALUE] [--pie] [-t TARGET_VOLATILITY] [-n]
+````
+
+Maximise return for a target risk. The resulting portfolio will have a volatility less than the target (but not guaranteed to be equal).
+
+* -p : Amount of time to retrieve data from yfinance. Default: 3mo.
+* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights.
+* --pie : Display a pie chart for weights
+* -t : The desired maximum volatility of the resultingportfolio
+* -n : Whether the portfolio should be market neutral (weights sum to zero). Defaults: False.
+
+
+
+
+
+### ef
+
+````
+usage: ef [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-n N_PORT]
+````
+
+This function plots random portfolios based on their risk and returns and shows the efficient frontier.
+
+* -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.
+
+
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]
+## 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()