diff --git a/README.md b/README.md
index 54c5089fe7f..84a6007fdd1 100644
--- a/README.md
+++ b/README.md
@@ -257,10 +257,14 @@ These are the ones where a key is necessary:
* Financial Modeling Prep: https://financialmodelingprep.com/developer
* FRED: https://fred.stlouisfed.org/docs/api/api_key.html
* News API: https://newsapi.org
+ * Tradier: https://developer.tradier.com/getting_started
When these are obtained, don't forget to update [config_terminal.py](/gamestonk_terminal/config_terminal.py).
-Alternatively, you can also set them to the following environment variables: GT_API_KEY_ALPHAVANTAGE, GT_API_KEY_FINANCIALMODELINGPREP, GT_API_KEY_QUANDL, GT_API_REDDIT_CLIENT_ID, GT_API_REDDIT_CLIENT_SECRET, GT_API_REDDIT_USERNAME, GT_API_REDDIT_USER_AGENT, GT_API_REDDIT_PASSWORD, GT_API_TWITTER_KEY, GT_API_TWITTER_SECRET_KEY, GT_API_TWITTER_BEARER_TOKEN, GT_API_POLYGON_KEY, GT_FRED_API_KEY, GT_API_NEWS_TOKEN.
+Alternatively, you can also set them to the following environment variables: GT_API_KEY_ALPHAVANTAGE,
+GT_API_KEY_FINANCIALMODELINGPREP, GT_API_KEY_QUANDL, GT_API_REDDIT_CLIENT_ID, GT_API_REDDIT_CLIENT_SECRET, GT_API_REDDIT_USERNAME,
+GT_API_REDDIT_USER_AGENT, GT_API_REDDIT_PASSWORD, GT_API_TWITTER_KEY, GT_API_TWITTER_SECRET_KEY,
+GT_API_TWITTER_BEARER_TOKEN, GT_API_POLYGON_KEY, GT_FRED_API_KEY, GT_API_NEWS_TOKEN, GT_TRADIER_TOKEN .
Example:
```
diff --git a/ROADMAP.md b/ROADMAP.md
index 86e76c6d2ac..82d5b910ed9 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -14,6 +14,7 @@
* [Exploratory Data Analysis](#Exploratory-Data-Analysis)
* [Residual Analysis](#Residual-Analysis)
* [Portfolio Analysis](#Portfolio-Analysis)
+* [Portfolio Optimization](#Portfolio-Optimization)
* [FRED](#FRED)
* [Options](#Options)
* [Credit Analysis](#Credit-Analysis)
@@ -33,7 +34,8 @@
* [x] Top Losers (@didier) - [PR #171](https://github.com/DidierRLopes/GamestonkTerminal/pull/171)
* [x] ARK orders (@aia) - [PR #140](https://github.com/DidierRLopes/GamestonkTerminal/pull/140)
* [x] Analyse sectors/industry performance, overview and valuation from Finviz (@didier) - [PR #315](https://github.com/DidierRLopes/GamestonkTerminal/pull/315)
-* [x] Add top ratings updates (@hinxx) - [PR #350](https://github.com/DidierRLopes/GamestonkTerminal/pull/350)
+* [x] Add latest and trending news commands (@hinxx) - [PR #347](https://github.com/DidierRLopes/GamestonkTerminal/pull/347)
+* [x] Add top ratings updates (@hinxx) - [PR #357](https://github.com/DidierRLopes/GamestonkTerminal/pull/357)
**NEXT**
@@ -111,6 +113,7 @@
* [x] Add multi-ticker sentiment comparison (@didier) - [PR #250](https://github.com/DidierRLopes/GamestonkTerminal/pull/250)
* [x] Get similar companies from Finviz based on Industry and Sector (and possibly country) from Finviz (@didier) - [PR #323](https://github.com/DidierRLopes/GamestonkTerminal/pull/323)
* [x] Output brief screen (overview, performance, financial, valuation) across similar companies (@didier) - [PR #323](https://github.com/DidierRLopes/GamestonkTerminal/pull/323)
+* [x] Add command to take tickers to portfolio optimization (@jmaslek) - [PR #329](https://github.com/DidierRLopes/GamestonkTerminal/pull/329)
**NEXT**
@@ -143,13 +146,17 @@
* [ ] Add Brokers (td, webull, etc) (@jmaslek)
* [ ] Refactoring
* [ ] Summaries / tear sheets (@jmaslek)
+
___
-##Portfolio Optimization
+
+## Portfolio Optimization
* [x] Basic Optimization through PyPortFolioOpt(@jmaslek) - [PR #329](https://github.com/DidierRLopes/GamestonkTerminal/pull/329)
+* [x] Add command to maximise the quadratic utility(@didier) - [PR #349](https://github.com/DidierRLopes/GamestonkTerminal/pull/349)
**NEXT**
-* [ ] Decide what features should be present
* [ ] Allow for more custom optimization constrains
+
+
---
## FRED
@@ -190,6 +197,7 @@ ___
* [x] Add README with explanation of how presets are stored and can be added by experienced users. (@didier) - [PR #314](https://github.com/DidierRLopes/GamestonkTerminal/pull/314)
* [x] Add screener signals (e.g. top gainers, new highs, most volatile, oversold, major news, ...) from Finviz (@didier) - [PR #314](https://github.com/DidierRLopes/GamestonkTerminal/pull/314)
* [x] Plot screener historical using Yahoo Finance data (@didier) - [PR #319](https://github.com/DidierRLopes/GamestonkTerminal/pull/319)
+* [x] Add command to take tickers to portfolio optimization (@didier) - [PR #349](https://github.com/DidierRLopes/GamestonkTerminal/pull/349)
**NEXT**
diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md
index e41212591eb..192f21ca288 100644
--- a/gamestonk_terminal/README.md
+++ b/gamestonk_terminal/README.md
@@ -9,6 +9,7 @@
* [Due Diligence](#Due-Diligence-)
* [Prediction Techniques](#Prediction-Techniques-)
* [Portfolio Analysis](#Portfolio-Analysis-)
+* [Portfolio Optimization](#Portfolio-Optimization-)
* [Cryptocurrencies](#Cryptocurrencies-)
* [Comparison Analysis](#Comparison-Analysis-)
* [Exploratory Data Analysis](#Exploratory-Data-Analysis-)
@@ -79,6 +80,8 @@ Command|Description|Source
`valuation` |valuation of sectors, industry, country |[Finviz](https://finviz.com)
`performance` |performance of sectors, industry, country |[Finviz](https://finviz.com)
`spectrum` |spectrum of sectors, industry, country |[Finviz](https://finviz.com)
+`latest` |latest news |[Seeking Alpha](https://seekingalpha.com/)
+`trending` |trending news |[Seeking Alpha](https://seekingalpha.com/)
`ratings ` |top ratings updates |[MarketBeat](https://marketbeat.com)
@@ -243,16 +246,19 @@ Command|Description|Brokers
## Portfolio Optimization [»](portfolio_optimization/README.md)
-command|desciription
+
+Command|Description
------|------
-`equal_weight`|Returns equal weights for all selected stocks
-`mkt_cap`| Returns weights that are weighted by market cap
-`div_yield`|Returns weights that are weighted by dividend yield
-`max_sharpe`| Returns portfolio of Efficient Frontier with maximum sharpe ratio
-`min_vol`| Returns portfolio on Efficient Frontier with minimum volatility
-`eff_risk`| Returns portfolio on Efficient Frontier that maximizes returns at a given risk
-`eff_ret`| Returns portfolio on Efficient Frontier that minimizes risk at a given return level
-`show_eff`|Plots random portfolios and shows the Efficient Frontier
+`add`| add ticker to optimize
+`select`| overwrite current tickers with new tickers
+`equal`| equally weighted
+`property`| weight according to selected info property (e.g. marketCap)
+`maxsharpe`| optimizes for maximal Sharpe ratio (a.k.a the tangency portfolio)
+`minvol`| optimizes for minimum volatility
+`maxquadutil`| maximises the quadratic utility, given some risk aversion
+`effret`| maximises return for a given target risk
+`effrisk`| minimises risk for a given target return
+`ef`| show the efficient frontier
@@ -332,7 +338,9 @@ Command|Description
Command|Description|Source
------ | --------|----
`exp` | see/set expiry date
-`volume` | plot options trading volume / open interest |[Yahoo Finance](https://finance.yahoo.com/)
+`volume` | volume + open interest options trading plot |[Yahoo Finance](https://finance.yahoo.com/)
+`vcalls` | calls volume + open interest plot |[Yahoo Finance](https://finance.yahoo.com/)
+`vputs` | puts volume + open interest plot |[Yahoo Finance](https://finance.yahoo.com/)
diff --git a/gamestonk_terminal/comparison_analysis/README.md b/gamestonk_terminal/comparison_analysis/README.md
index f00b902183d..c60dfd305d0 100644
--- a/gamestonk_terminal/comparison_analysis/README.md
+++ b/gamestonk_terminal/comparison_analysis/README.md
@@ -32,6 +32,8 @@ This menu aims to compare a pre-loaded stock with similar companies, and the usa
* brief performance (e.g. Perf Week, Perf YTD, Volatility M) [Finviz]
* [technical](#technical)
* brief technical (e.g. Beta, SMA50, 52W Low, RSI, Change) [Finviz]
+* [> po](portfolio_optimization/README.md)
+ * portfolio optimization for selected tickers
## get
diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py
index f999ed092af..499b6c9cd39 100644
--- a/gamestonk_terminal/comparison_analysis/ca_controller.py
+++ b/gamestonk_terminal/comparison_analysis/ca_controller.py
@@ -84,8 +84,6 @@ class ComparisonAnalysisController:
if self.similar:
print(f"[{self.user}] Similar Companies: {', '.join(self.similar)}")
- # else:
- # print(f"No similar companies [{self.user}]")
print("\nComparison Analysis Mode:")
print(" help show this comparison analysis menu again")
@@ -110,8 +108,9 @@ class ComparisonAnalysisController:
print(" performance brief performance comparison")
print(" technical brief technical comparison")
print("")
- print(" > po portfolio optimization for selected tickers")
- print("")
+ if self.similar:
+ print(" > po portfolio optimization for selected tickers")
+ print("")
return
@staticmethod
@@ -320,7 +319,7 @@ class ComparisonAnalysisController:
finviz_compare_view.screener(other_args, "technical", self.ticker, self.similar)
def call_po(self, _):
- """Open Portfolio Optimization menu with ticker and similar"""
+ """Call the portfolio optimization menu with selected tickers"""
return po_controller.menu([self.ticker] + self.similar)
diff --git a/gamestonk_terminal/config_terminal.py b/gamestonk_terminal/config_terminal.py
index 4189a27ce4e..daf66a26be6 100644
--- a/gamestonk_terminal/config_terminal.py
+++ b/gamestonk_terminal/config_terminal.py
@@ -35,3 +35,6 @@ API_NEWS_TOKEN = os.getenv("GT_API_NEWS_TOKEN") or "REPLACE_ME"
# Robinhood
RH_USERNAME = os.getenv("GT_RH_USERNAME") or "REPLACE_ME"
RH_PASSWORD = os.getenv("GT_RH_PASSWORD") or "REPLACE_ME"
+
+# https://tradier.com/products/market-data-api
+TRADIER_TOKEN = os.getenv("GT_TRADIER_TOKEN") or "REPLACE_ME"
diff --git a/gamestonk_terminal/discovery/README.md b/gamestonk_terminal/discovery/README.md
index d1920d1975d..d892d25c1e7 100644
--- a/gamestonk_terminal/discovery/README.md
+++ b/gamestonk_terminal/discovery/README.md
@@ -32,6 +32,10 @@ This menu aims to discover new stocks, and the usage of the following commands a
* performance of sectors, industry, country [Finviz]
* [spectrum](#spectrum)
* spectrum of sectors, industry, country [Finviz]
+* [latest](#latest)
+ * latest news [Seeking Alpha]
+* [trending](#trending)
+ * trending news [Seeking Alpha]
* [ratings](#ratings)
* top ratings updates [MarketBeat]
@@ -222,6 +226,32 @@ Spectrum of sectors, industry, country. [Source: Finviz]

+## latest
+
+```
+usage: latest [-i N_ID] [-n N_NUM]
+```
+
+Latest news articles. [Source: Seeking Alpha]
+* -i : article number found on Seeking Alpha website
+* -n : number of latest articles being printed. Default 10.
+
+
+
+
+## trending
+
+```
+usage: trending [-i N_ID] [-n N_NUM]
+```
+
+Trending news articles. [Source: Seeking Alpha]
+* -i : article number found on Seeking Alpha website
+* -n : number of trending articles being printed. Default 10.
+
+
+
+
## ratings
```
@@ -234,4 +264,3 @@ Top ratings updates. [Source: MarketBeat]
-
diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py
index 6125a377773..2362e8f762b 100644
--- a/gamestonk_terminal/discovery/disc_controller.py
+++ b/gamestonk_terminal/discovery/disc_controller.py
@@ -48,6 +48,8 @@ class DiscoveryController:
"performance",
"spectrum",
"ratings",
+ "latest",
+ "trending",
]
def __init__(self):
@@ -89,6 +91,8 @@ class DiscoveryController:
print(" valuation valuation of sectors, industry, country [Finviz]")
print(" performance performance of sectors, industry, country [Finviz]")
print(" spectrum spectrum of sectors, industry, country [Finviz]")
+ print(" latest latest news [Seeking Alpha]")
+ print(" trending trending news [Seeking Alpha]")
print(" ratings top ratings updates [MarketBeat]")
print("")
@@ -190,6 +194,14 @@ class DiscoveryController:
other_args, "spectrum"
)
+ def call_latest(self, other_args: List[str]):
+ """Process latest command"""
+ seeking_alpha_view.latest_news_view(other_args)
+
+ def call_trending(self, other_args: List[str]):
+ """Process trending command"""
+ seeking_alpha_view.trending_news_view(other_args)
+
def call_ratings(self, other_args: List[str]):
"""Process ratings command"""
marketbeat_view.ratings_view(other_args)
diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py
index 59f63e44ae6..b573c506d3f 100755
--- a/gamestonk_terminal/discovery/seeking_alpha_model.py
+++ b/gamestonk_terminal/discovery/seeking_alpha_model.py
@@ -1,10 +1,12 @@
""" Seeking Alpha Model """
__docformat__ = "numpy"
+from typing import List
import requests
from bs4 import BeautifulSoup
import pandas as pd
from pandas.core.frame import DataFrame
+from datetime import datetime
from gamestonk_terminal.helper_funcs import get_user_agent
@@ -67,3 +69,141 @@ def get_next_earnings(pages: int) -> DataFrame:
df_earnings = df_earnings.set_index("Date")
return df_earnings
+
+
+def get_articles_html(url_articles: str) -> str:
+ """Wraps HTTP requests.get for testibility
+
+ Parameters
+ ----------
+ url_articles : str
+ Articles URL
+
+ Returns
+ -------
+ str
+ HTML page of articles
+ """
+ articles_html = requests.get(
+ url_articles, headers={"User-Agent": get_user_agent()}
+ ).text
+
+ return articles_html
+
+
+def get_article_list(num: int) -> List[dict]:
+ """Returns a list of latest articles
+
+ Parameters
+ ----------
+ pages : int
+ Number of articles
+
+ Returns
+ -------
+ list
+ Latest articles list
+ """
+
+ articles: List[dict] = []
+ page = 1
+ url_articles = f"https://seekingalpha.com/market-news/{page}"
+ while len(articles) < num:
+ text_soup_articles = BeautifulSoup(
+ get_articles_html(url_articles),
+ "lxml",
+ )
+
+ for item_row in text_soup_articles.findAll("li", {"class": "item"}):
+ item = item_row.find("a", {"class": "add-source-assigned"})
+ if item is None:
+ continue
+ article_url = item["href"]
+ if not article_url.startswith("/news/"):
+ continue
+ article_id = article_url.split("/")[2].split("-")[0]
+
+ articles.append(
+ {
+ "title": item.text,
+ "publishedAt": datetime.strptime(
+ item_row["data-last-date"], "%Y-%m-%d %H:%M:%S %z"
+ ).strftime("%Y-%m-%d %H:%M:%S"),
+ "url": "https://seekingalpha.com" + article_url,
+ "id": article_id,
+ }
+ )
+
+ page += 1
+ url_articles = f"https://seekingalpha.com/market-news/{page}"
+
+ return articles
+
+
+def get_trending_list(num: int) -> list:
+ """Returns a list of trending articles
+
+ Parameters
+ ----------
+ pages : int
+ Number of articles
+
+ Returns
+ -------
+ list
+ Trending articles list
+ """
+
+ articles = list()
+ url_articles = "https://seekingalpha.com/news/trending_news"
+ response = requests.get(url_articles, headers={"User-Agent": get_user_agent()})
+
+ # Check that the API response was successful
+ if response.status_code != 200:
+ print("Invalid response\n")
+ else:
+ for item in response.json():
+ article_url = item["uri"]
+ if not article_url.startswith("/news/"):
+ continue
+ article_id = article_url.split("/")[2].split("-")[0]
+ articles.append(
+ {
+ "title": item["title"],
+ "publishedAt": item["publish_on"][: item["publish_on"].rfind(".")],
+ "url": "https://seekingalpha.com" + article_url,
+ "id": article_id,
+ }
+ )
+
+ return articles
+
+
+def get_article_data(article_id: int) -> dict:
+ """Returns an article
+
+ Parameters
+ ----------
+ article_id : int
+ Article ID
+
+ Returns
+ -------
+ dict
+ Article data
+ """
+
+ article_url = f"https://seekingalpha.com/api/v3/news/{article_id}"
+ response = requests.get(article_url, headers={"User-Agent": get_user_agent()})
+ jdata = response.json()
+ content = jdata["data"]["attributes"]["content"].replace("", "\n")
+ content = BeautifulSoup(content, features="html.parser").get_text()
+
+ article = {
+ "title": jdata["data"]["attributes"]["title"],
+ "publishedAt": jdata["data"]["attributes"]["lastModified"],
+ "url": "https://seekingalpha.com" + jdata["data"]["links"]["self"],
+ "content": content,
+ }
+
+ return article
diff --git a/gamestonk_terminal/discovery/seeking_alpha_view.py b/gamestonk_terminal/discovery/seeking_alpha_view.py
index 2965bf79cce..a11c548be72 100644
--- a/gamestonk_terminal/discovery/seeking_alpha_view.py
+++ b/gamestonk_terminal/discovery/seeking_alpha_view.py
@@ -65,3 +65,147 @@ def earnings_release_dates_view(other_args: List[str]):
].to_string(index=False, header=False)
)
print("")
+
+
+def latest_news_view(other_args: List[str]):
+ """Prints the latest news article list
+
+ Parameters
+ ----------
+ other_args : List[str]
+ argparse other args - ["-i", "123123", "-n", "5"]
+ """
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog="latest",
+ description="""Latest news articles. [Source: Seeking Alpha]""",
+ )
+ parser.add_argument(
+ "-i",
+ "--id",
+ action="store",
+ dest="n_id",
+ type=check_positive,
+ default=-1,
+ help="article number found on Seeking Alpha website",
+ )
+ parser.add_argument(
+ "-n",
+ "--num",
+ action="store",
+ dest="n_num",
+ type=check_positive,
+ default=10,
+ help="number of latest articles being printed",
+ )
+
+ if other_args:
+ if "-" not in other_args[0]:
+ other_args.insert(0, "-i")
+
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return
+
+ # User wants to see all latest news
+ if ns_parser.n_id == -1:
+ articles = seeking_alpha_model.get_article_list(ns_parser.n_num)
+ for idx, article in enumerate(articles):
+ print(
+ article["publishedAt"].replace("T", " ").replace("Z", ""),
+ "-",
+ article["id"],
+ "-",
+ article["title"],
+ )
+ print(article["url"])
+ print("")
+
+ if idx >= ns_parser.n_num - 1:
+ break
+
+ # User wants to access specific article
+ else:
+ article = seeking_alpha_model.get_article_data(ns_parser.n_id)
+ print(
+ article["publishedAt"][: article["publishedAt"].rfind(":") - 3].replace(
+ "T", " "
+ ),
+ " ",
+ article["title"],
+ )
+ print(article["url"])
+ print("")
+ print(article["content"])
+
+
+def trending_news_view(other_args: List[str]):
+ """Prints the trending news article list
+
+ Parameters
+ ----------
+ other_args : List[str]
+ argparse other args - ["i", "123123", "-n", "5"]
+ """
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog="trending",
+ description="""Trending news articles. [Source: Seeking Alpha]""",
+ )
+ parser.add_argument(
+ "-i",
+ "--id",
+ action="store",
+ dest="n_id",
+ type=check_positive,
+ default=-1,
+ help="article number found on Seeking Alpha website",
+ )
+ parser.add_argument(
+ "-n",
+ "--num",
+ action="store",
+ dest="n_num",
+ type=check_positive,
+ default=10,
+ help="number of trending articles being printed",
+ )
+
+ if other_args:
+ if "-" not in other_args[0]:
+ other_args.insert(0, "-i")
+
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return
+
+ # User wants to see all trending articles
+ if ns_parser.n_id == -1:
+ articles = seeking_alpha_model.get_trending_list(ns_parser.n_num)
+ for idx, article in enumerate(articles):
+ print(
+ article["publishedAt"].replace("T", " ").replace("Z", ""),
+ "-",
+ article["id"],
+ "-",
+ article["title"],
+ )
+ print(article["url"])
+ print("")
+
+ if idx >= ns_parser.n_num - 1:
+ break
+
+ # User wants to access specific article
+ else:
+ article = seeking_alpha_model.get_article_data(ns_parser.n_id)
+ print(
+ article["publishedAt"][: article["publishedAt"].rfind(":") - 3].replace(
+ "T", " "
+ ),
+ " ",
+ article["title"],
+ )
+ print(article["url"])
+ print("")
+ print(article["content"])
diff --git a/gamestonk_terminal/helper_funcs.py b/gamestonk_terminal/helper_funcs.py
index 11a3ffbe196..a2e00fd3d35 100644
--- a/gamestonk_terminal/helper_funcs.py
+++ b/gamestonk_terminal/helper_funcs.py
@@ -347,7 +347,7 @@ def patch_pandas_text_adjustment():
def parse_known_args_and_warn(parser, l_args):
parser.add_argument(
- "-h", "--help", action="store_true", dest="help", help="show this help message"
+ "-h", "--help", action="store_true", help="show this help message"
)
(ns_parser, l_unknown_args) = parser.parse_known_args(l_args)
diff --git a/gamestonk_terminal/options/README.md b/gamestonk_terminal/options/README.md
index a86dceb2b2c..980be7495c4 100644
--- a/gamestonk_terminal/options/README.md
+++ b/gamestonk_terminal/options/README.md
@@ -1,11 +1,13 @@
# OPTIONS DATA
-This menu aims to give insight on options. options can cause significant share price movement, and option pricing gives info about market sentiment for a ticker.
+This menu aims to give insight on options. options can cause significant share price movement, and option pricing gives
+info about market sentiment for a ticker.
* [volume](#volume)
* Show traded options for a given expiry [Yahoo Finance]
-
+* [chains](#chains)
+ * Display option chains for
## volume
@@ -23,3 +25,26 @@ usage: volume --expiry yyyy-mm-dd
Display volume graph for a date. [Source: Yahoo Finance]
* -e, --expiry : expiration date.
+
+## chains
+Display a table of options chains.
+
+This data requires a Tradier sandbox developer account, which can be made [here](#https://tradier.com/products/market-data-api)
+
+````
+usage: chains [--calls] [--puts] [-m MIN] [-M MAX]
+````
+* --calls : Flag to only display call options
+* --puts : Flag to display only put options
+* -m : Minimum strike price to show
+* -M : Maximum strike price to show
+
+For combines tables, call options are on the left and put options are on the right of the strike
+
+As an example, if I load my ticker and select an expiry, to view the options chain for strikes between $129 and $135, one would use:
+
+````
+chains -m 129 -M 135
+````
+Which outputs:
+
diff --git a/gamestonk_terminal/options/chains_view.py b/gamestonk_terminal/options/chains_view.py
new file mode 100644
index 00000000000..c05552a909e
--- /dev/null
+++ b/gamestonk_terminal/options/chains_view.py
@@ -0,0 +1,208 @@
+"""Helper functions for the options menu"""
+__docformat__ = "numpy"
+
+import argparse
+from typing import List
+import requests
+import pandas as pd
+import numpy as np
+from tabulate import tabulate
+from gamestonk_terminal.config_terminal import TRADIER_TOKEN
+from gamestonk_terminal.helper_funcs import (
+ check_non_negative,
+ parse_known_args_and_warn,
+)
+
+option_columns = [
+ "bid",
+ "ask",
+ "strike",
+ "bidsize",
+ "asksize",
+ "volume",
+ "open_interest",
+ "option_type",
+]
+greek_columns = ["delta", "gamma", "theta", "vega", "ask_iv", "bid_iv", "mid_iv"]
+df_columns = option_columns + greek_columns
+column_map = {"mid_iv": "iv", "open_interest": "oi", "volume": "vol"}
+
+
+def process_chains(response: requests.models.Response) -> pd.DataFrame:
+ """
+ Function to take in the requests.get and return a DataFrame
+ Parameters
+ ----------
+ response: requests.models.Response
+ This is the respnose from tradier api.
+
+ Returns
+ -------
+ opt_chain: pd.DataFrame
+ Dataframe with all available options
+ """
+ json_response = response.json()
+ options = json_response["options"]["option"]
+
+ opt_chain = pd.DataFrame(columns=df_columns)
+ for idx, option in enumerate(options):
+ data = [option[col] for col in option_columns]
+ data += [option["greeks"][col] for col in greek_columns]
+ opt_chain.loc[idx, :] = data
+
+ return opt_chain
+
+
+def get_option_chains(symbol: str, expiry: str) -> pd.DataFrame:
+ """
+
+ Parameters
+ ----------
+ symbol : str
+ Ticker to get options for
+ expiry : str
+ Expiration date in the form of "YYYY-MM-DD"
+
+ Returns
+ -------
+ chains: pd.DataFrame
+ Dataframe with options for the gievn Symbol and Expiration date
+ """
+ params = {"symbol": symbol, "expiration": expiry, "greeks": "true"}
+
+ headers = {"Authorization": f"Bearer {TRADIER_TOKEN}", "Accept": "application/json"}
+
+ response = requests.get(
+ "https://sandbox.tradier.com/v1/markets/options/chains",
+ params=params,
+ headers=headers,
+ )
+ if response.status_code == 401:
+ print("Error in request.get -- check token")
+ return
+
+ chains = process_chains(response)
+ return chains
+
+
+def display_chains(symbol: str, expiry: str, other_args: List[str]):
+
+ parser = argparse.ArgumentParser(
+ prog="chains", add_help=False, description="Display options chains"
+ )
+
+ parser.add_argument(
+ "--calls",
+ action="store_true",
+ default=False,
+ dest="calls_only",
+ help="Flag to show calls only",
+ )
+
+ parser.add_argument(
+ "--puts",
+ action="store_true",
+ default=False,
+ dest="puts_only",
+ help="Flag to show puts only",
+ )
+ parser.add_argument(
+ "-m",
+ "--min",
+ dest="min_sp",
+ type=check_non_negative,
+ default=-1,
+ help="minimum strike price to consider.",
+ )
+ parser.add_argument(
+ "-M",
+ "--max",
+ dest="max_sp",
+ type=check_non_negative,
+ default=-1,
+ help="maximum strike price to consider.",
+ )
+
+ chains_df = get_option_chains(symbol, expiry)
+
+ default_columns = [
+ "mid_iv",
+ "delta",
+ "volume",
+ "open_interest",
+ "bid",
+ "ask",
+ "strike",
+ "option_type",
+ ]
+ chains_df = chains_df[default_columns].rename(columns=column_map)
+
+ try:
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return
+ if ns_parser.max_sp == -1 and ns_parser.min_sp == -1:
+ min_strike = np.percentile(chains_df["strike"], 25)
+ max_strike = np.percentile(chains_df["strike"], 75)
+ else:
+ min_strike = ns_parser.min_sp
+ max_strike = ns_parser.max_sp
+
+ chains_df = chains_df[chains_df["strike"] >= min_strike]
+ chains_df = chains_df[chains_df["strike"] <= max_strike]
+ calls_df = chains_df[chains_df.option_type == "call"].drop(
+ columns=["option_type"]
+ )
+ puts_df = chains_df[chains_df.option_type == "put"].drop(
+ columns=["option_type"]
+ )
+
+ if ns_parser.calls_only:
+ print(
+ tabulate(
+ calls_df,
+ headers=calls_df.columns,
+ tablefmt="grid",
+ showindex=False,
+ floatfmt=".2f",
+ )
+ )
+ return
+
+ if ns_parser.puts_only:
+ print(
+ tabulate(
+ puts_df,
+ headers=puts_df.columns,
+ tablefmt="grid",
+ showindex=False,
+ floatfmt=".2f",
+ )
+ )
+ return
+
+ puts_df = puts_df[puts_df.columns[::-1]]
+ chain_table = calls_df.merge(puts_df, on="strike")
+
+ headers = [
+ col.strip("_x")
+ if col.endswith("_x")
+ else col.strip("_y")
+ if col.endswith("_y")
+ else col
+ for col in chain_table.columns
+ ]
+ print(
+ tabulate(
+ chain_table,
+ headers=headers,
+ tablefmt="fancy_grid",
+ showindex=False,
+ floatfmt=".2f",
+ )
+ )
+
+ except Exception as e:
+ print(e)
+ print("")
+ return
diff --git a/gamestonk_terminal/options/op_controller.py b/gamestonk_terminal/options/op_controller.py
index e373ba1f677..4caa51dbd63 100644
--- a/gamestonk_terminal/options/op_controller.py
+++ b/gamestonk_terminal/options/op_controller.py
@@ -9,7 +9,7 @@ import yfinance as yf
from prompt_toolkit.completion import NestedCompleter
from gamestonk_terminal.helper_funcs import get_flair, parse_known_args_and_warn
from gamestonk_terminal import feature_flags as gtff
-from gamestonk_terminal.options import volume_view
+from gamestonk_terminal.options import volume_view, chains_view
from gamestonk_terminal.menu import session
@@ -17,7 +17,7 @@ class OptionsController:
"""Options Controller class."""
# Command choices
- CHOICES = ["help", "q", "quit", "exp", "voi", "vcalls", "vputs"]
+ CHOICES = ["help", "q", "quit", "exp", "voi", "vcalls", "vputs", "chains"]
def __init__(self, ticker: str, last_adj_close_price: float):
"""Construct data."""
@@ -93,6 +93,8 @@ class OptionsController:
print(" vcalls calls volume + open interest plot")
print(" vputs puts volume + open interest plot")
print("")
+ print(" chains display option chains")
+ print("")
return
def switch(self, an_input: str):
@@ -158,6 +160,9 @@ class OptionsController:
self.options.puts,
)
+ def call_chains(self, other_args):
+ chains_view.display_chains(self.ticker, self.expiry_date, other_args)
+
def menu(ticker: str, last_adj_close_price: float):
""" Options Menu. """
diff --git a/gamestonk_terminal/options/volume_view.py b/gamestonk_terminal/options/volume_view.py
index a20e4cafc54..a4f3062a960 100644
--- a/gamestonk_terminal/options/volume_view.py
+++ b/gamestonk_terminal/options/volume_view.py
@@ -2,12 +2,12 @@
__docformat__ = "numpy"
from bisect import bisect_left
-import seaborn as sns
from typing import List
import argparse
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
+import seaborn as sns
from gamestonk_terminal.helper_funcs import (
plot_autoscale,
diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md
index bf96cf77629..3e9e44d4603 100644
--- a/gamestonk_terminal/portfolio_optimization/README.md
+++ b/gamestonk_terminal/portfolio_optimization/README.md
@@ -1,169 +1,224 @@
# Portfolio Optimization
-This menu aims to discover optimal portfolios for selected stocks.
+This menu aims to optimize a portfolio of pre-loaded stocks, and the usage of the following commands along with an example will be exploited below.
-Currently we support two methods:
+* [add](#add)
+ * add ticker to optimize
+* [select](#select)
+ * overwrite current tickers with new tickers
+* [equal](#equal)
+ * equally weighted
+* [mktcap](#mktcap)
+ * weighted according to market cap (property marketCap)
+* [dividend](#dividend)
+ * weighted according to dividend yield (property dividendYield)
+* [property](#property)
+ * weight according to selected info property
-* [Property Weighting](#weighting)
- * Equal Weights
- * Market Cap Weighting
- * Divident Yield Weighting
-* [Mean-Variance-Optimization](#eff_front)
- * Max sharpe ratio
- * Minimum volatility
- * Maximum returns at given risk level
- * Minimum risk level at a given return
+[Mean Variance Optimization](#Mean_Variance_Optimization)
-## Procedure
-There are three ways to load stocks to be analyzed.
-* add
-* select
-* from ca menu
-###add
-Adds selected tickers to the menu to be considered
+* [maxsharpe](#maxsharpe)
+ * optimizes for maximal Sharpe ratio (a.k.a the tangency portfolio)
+* [minvol](#minvol)
+ * optimizes for minimum volatility
+* [maxquadutil](#maxquadutil)
+ * maximises the quadratic utility, given some risk aversion
+* [effret](#effret)
+ * maximises return for a given target risk
+* [effrisk](#effrisk)
+ * minimises risk for a given target return
+* [ef](#ef)
+ * show the efficient frontier
-````
-usage: add ticker1,ticker2,ticker3,...
-````
-###select
-Clears current list and loads selected ticker
-````
-usage: select ticker1,ticker2,ticker3,...
-````
-### ca menu
-From the ca menu, the loaded ticker and the selected similar tickers can be loaded by entering the `> po` menu.
+### add
-From this `po` menu, you can return to `ca` using `ca`, which will keep your loaded stock, but reset the selected stocks from that menu.
+```text
+add [-t ADD_TICKERS]
+```
-Once your stocks are listed, you can select one of the options.
-## Property weighted
-* equal weights
-* market cap weighted
-* dividend yield weighted
-### equal weights
-Returns an equally weighted portfolio where the weights are 1/number of stocks.
-````
-equal_weight [-v --value VALUE] [--pie]
-````
-* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights.
-* --pie Flag that displays a pie chart of the allocations.
+Add/Select tickers for portfolio to be optimized.
-### market cap weighted
-Returns portfolio values that are weighted by their relative Market Cap.
-````
-mkt_cap [-v --value VALUE] [--pie]
-````
-* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights.
-* --pie Flag that displays a pie chart of the allocations.
+* -t : Tickers to be used in the portfolio to optimize
+*
-###dividend yield weighted
-Returns portfolio values that are weighted by relative Dividend Yield.
-````
-div_yield [-v --value VALUE] [--pie]
-````
-* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights.
-* --pie Flag that displays a pie chart of the allocations.
+### select
-## Mean Variance Optimization
+```text
+select [-t ADD_TICKERS]
+```
+
+Add/Select tickers for portfolio to be optimized.
+
+* -t : Tickers to be used in the portfolio to optimize
+
+
+### equal
+
+```text
+equal [-v VALUE] [--pie]
+```
+
+Returns an equally weighted portfolio
+
+* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights.
+* --pie : Display a pie chart for weights
+
+
+
+
+### 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]
````
-* -p/--period Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo.
-* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights.
-* --pie Flag that displays a pie chart of the allocations.
-### min_vol
This portfolio minimizes the total volatility, which also means it has the smallest returns among the efficient frontier.
-The usage is:
-````
-min_vol [-p PERIOD] [-v --value VALUE] [--pie]
-````
-* -p/--period Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo.
-* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights.
-* --pie Flag that displays a pie chart of the allocations.
-### eff_risk
-This portfolio maximizes the returns at a given risk tolerance
-The usage is:
-````
-eff_risk [-p PERIOD] [-r --risk RISK_LEVEL] [-v --value VALUE] [--pie]
-````
-* -p/--period Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo.
-* -r/--risk Risk tolerance. Default is 0.1 (10%)
-* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights.
-* --pie Flag that displays a pie chart of the allocations.
+* -p : Amount of time to retrieve data from yfinance. Default: 3mo.
+* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights.
+* --pie : Display a pie chart for weights
-### eff_ret
-This portfolio minimizes the risk at a given return level
-The usage is:
-````
-eff_ret [-p PERIOD] [-r --return] [-v --value VALUE] [--pie]
-````
-* -p/--period Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo.
-* -r/--return. Desired return. Default is 0.1 (10%)
-* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights.
-* --pie Flag that displays a pie chart of the allocations.
+
+
+
+
+### 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]
+````
-### show_eff
This function plots random portfolios based on their risk and returns and shows the efficient frontier.
-The usage is:
-````
-show_eff [-p PERIOD] [-n N_PORTFOLIOS]
-````
-* -p/--period Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo.
-* -n Number of portfolios to simulate.
+
+* -n : Number of portfolios to simulate. Default 300.
+* -p : Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo.
-##Sample Usage
-In this example, we generate weights for a list of 6 stocks using the eff_ret command. This optimization looks to maximize returns
-at a given risk level. We start by adding the stocks we want to analyze:
-````
-select aapl,amzn,msft,f,gm,ge
-````
-Which shows:
-````
-Current Tickers: GE, GM, AMZN, AAPL, F, MSFT
-````
-To perform the optimization, we will set a target return of 25% (.25). Given how stocks performed during COVID,
-there is a lot of volatility, so many optimizations may not get low volatility. The module also returns annualized volatility,
-so the number is your portfolio volatility * `sqrt(252`. This optimization will be (including a pie chart!). Note we could also supply a different
-time period, which changes the expected returns and historical volatility, which changes the optimization. We could also specify a dollar
-amount that you wish to allocate using the `-v` flag.
-````
-eff_ret -r .25 --pie
-````
-The console will show (numbers will vary based on when this is done).
-
-
-And the pie chart:
-
-
-
-Note that since `AAPL` had zero allocation, it was omitted from the chart.
\ No newline at end of file
+
diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py
index 3ca81f396a2..f88ad80463c 100644
--- a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py
+++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py
@@ -13,21 +13,113 @@ from pypfopt import risk_models
from pypfopt import expected_returns
from gamestonk_terminal.config_plot import PLOT_DPI
from gamestonk_terminal import feature_flags as gtff
-from gamestonk_terminal.helper_funcs import plot_autoscale, parse_known_args_and_warn
+from gamestonk_terminal.helper_funcs import plot_autoscale
-title_maps = {
- "max_sharpe": "Maximum Sharpe Portfolio",
- "min_volatility": "Minimum Volatility Portfolio",
- "eff_risk": "Maximum Return Portfolio at Risk = {:1f} %",
- "eff_ret": "Minimum Volatility Portfolio at Target Return = {:.1f} %",
- "equal": "Equally Weighted Portfolio",
- "marketCap": "MarketCap Weighted Portfolio",
- "dividendYield": "Dividend Yield Weighted Portfolio",
-}
+l_valid_property_infos = [
+ "previousClose",
+ "regularMarketOpen",
+ "twoHundredDayAverage",
+ "trailingAnnualDividendYield",
+ "payoutRatio",
+ "volume24Hr",
+ "regularMarketDayHigh",
+ "navPrice",
+ "averageDailyVolume10Day",
+ "totalAssets",
+ "regularMarketPreviousClose",
+ "fiftyDayAverage",
+ "trailingAnnualDividendRate",
+ "open",
+ "toCurrency",
+ "averageVolume10days",
+ "expireDate",
+ "yield",
+ "algorithm",
+ "dividendRate",
+ "exDividendDate",
+ "beta",
+ "circulatingSupply",
+ "regularMarketDayLow",
+ "priceHint",
+ "currency",
+ "trailingPE",
+ "regularMarketVolume",
+ "lastMarket",
+ "maxSupply",
+ "openInterest",
+ "marketCap",
+ "volumeAllCurrencies",
+ "strikePrice",
+ "averageVolume",
+ "priceToSalesTrailing12Months",
+ "dayLow",
+ "ask",
+ "ytdReturn",
+ "askSize",
+ "volume",
+ "fiftyTwoWeekHigh",
+ "forwardPE",
+ "fromCurrency",
+ "fiveYearAvgDividendYield",
+ "fiftyTwoWeekLow",
+ "bid",
+ "dividendYield",
+ "bidSize",
+ "dayHigh",
+ "annualHoldingsTurnover",
+ "enterpriseToRevenue",
+ "beta3Year",
+ "profitMargins",
+ "enterpriseToEbitda",
+ "52WeekChange",
+ "morningStarRiskRating",
+ "forwardEps",
+ "revenueQuarterlyGrowth",
+ "sharesOutstanding",
+ "fundInceptionDate",
+ "annualReportExpenseRatio",
+ "bookValue",
+ "sharesShort",
+ "sharesPercentSharesOut",
+ "fundFamily",
+ "lastFiscalYearEnd",
+ "heldPercentInstitutions",
+ "netIncomeToCommon",
+ "trailingEps",
+ "lastDividendValue",
+ "SandP52WeekChange",
+ "priceToBook",
+ "heldPercentInsiders",
+ "shortRatio",
+ "sharesShortPreviousMonthDate",
+ "floatShares",
+ "enterpriseValue",
+ "threeYearAverageReturn",
+ "lastSplitFactor",
+ "legalType",
+ "lastDividendDate",
+ "morningStarOverallRating",
+ "earningsQuarterlyGrowth",
+ "pegRatio",
+ "lastCapGain",
+ "shortPercentOfFloat",
+ "sharesShortPriorMonth",
+ "impliedSharesOutstanding",
+ "fiveYearAverageReturn",
+ "regularMarketPrice",
+]
+
+
+def check_valid_property_type(property: str) -> str:
+ """ Check that the property selected is valid """
+ if property in l_valid_property_infos:
+ return property
+ else:
+ raise argparse.ArgumentTypeError(f"{property} is not a valid info")
def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFrame:
- """
+ """Get adjusted closing price for each stock in the list
Parameters
----------
@@ -51,8 +143,8 @@ def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFra
def prepare_efficient_frontier(stock_prices: pd.DataFrame):
- """
- Take in a dataframe of prices and return an efficient frontier object
+ """Take in a dataframe of prices and return an efficient frontier object
+
Parameters
----------
stock_prices : DataFrame
@@ -82,11 +174,11 @@ def display_weights(weights: dict):
weight_df = pd.DataFrame.from_dict(data=weights, orient="index", columns=["value"])
if math.isclose(weight_df.sum()["value"], 1, rel_tol=0.1):
weight_df["weight"] = (weight_df["value"] * 100).astype(str).apply(
- lambda s: s[:6]
+ lambda s: " " + s[:4] if s.find(".") == 1 else "" + s[:5]
) + " %"
- print(pd.DataFrame(weight_df["weight"]))
+ print(pd.DataFrame(weight_df["weight"]).to_string(header=False))
else:
- print(weight_df)
+ print(weight_df.to_string(header=False))
def my_autopct(x):
@@ -97,17 +189,16 @@ def my_autopct(x):
return ""
-def pie_chart_weights(weights: dict, optimizer: str, value: float):
- """
- Show a pie chart of holdings
+def pie_chart_weights(weights: dict, title_opt: str):
+ """Show a pie chart of holdings
+
Parameters
----------
weights: dict
- weights to display. Keys are stocks. Values are either weights or values if -v specified
- optimzer: str
- Optimization technique used for title
+ Weights to display, where keys are tickers, and values are either weights or values if -v specified
+ title: str
+ Title to be used on the plot title
"""
- plt.close("all")
if not weights:
return
@@ -122,31 +213,28 @@ def pie_chart_weights(weights: dict, optimizer: str, value: float):
total_size = np.sum(sizes)
- leg_labels = [
- f"{str(a)}: {str(round(100*b/total_size,3))[:4]}%"
- for a, b in zip(stocks, sizes)
- ]
plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI)
+
if math.isclose(sum(sizes), 1, rel_tol=0.1):
wedges, _, autotexts = plt.pie(
sizes,
labels=stocks,
autopct=my_autopct,
textprops=dict(color="k"),
- explode=[s / (5 * total_size) for s in sizes],
+ wedgeprops={"linewidth": 3.0, "edgecolor": "white"},
normalize=True,
- shadow=True,
)
+ plt.setp(autotexts, color="white", fontweight="bold")
else:
wedges, _, autotexts = plt.pie(
sizes,
labels=stocks,
autopct="",
textprops=dict(color="k"),
- explode=[s / (5 * total_size) for s in sizes],
+ wedgeprops={"linewidth": 3.0, "edgecolor": "white"},
normalize=True,
- shadow=True,
)
+ plt.setp(autotexts, color="white", fontweight="bold")
for i, a in enumerate(autotexts):
if sizes[i] / total_size > 0.05:
a.set_text(f"{sizes[i]:.2f}")
@@ -155,72 +243,37 @@ def pie_chart_weights(weights: dict, optimizer: str, value: float):
plt.axis("equal")
- plt.legend(
+ leg1 = plt.legend(
wedges,
- leg_labels,
- title="Stocks",
- loc="center left",
- bbox_to_anchor=(0.85, 0, 0.5, 1),
+ [str(s) for s in stocks],
+ title=" Ticker",
+ loc="upper left",
+ bbox_to_anchor=(0.80, 0, 0.5, 1),
+ frameon=False,
)
+ leg2 = plt.legend(
+ wedges,
+ [
+ f"{' ' if ((100*s/total_size) < 10) else ''}{100*s/total_size:.2f}%"
+ for s in sizes
+ ],
+ title=" ",
+ loc="upper left",
+ handlelength=0,
+ bbox_to_anchor=(0.91, 0, 0.5, 1),
+ frameon=False,
+ )
+ plt.gca().add_artist(leg1)
+ plt.gca().add_artist(leg2)
plt.setp(autotexts, size=8, weight="bold")
- if optimizer in ["eff_ret", "eff_risk"]:
- plt.title(title_maps[optimizer].format(100 * value))
- else:
- plt.title(title_maps[optimizer])
+ plt.gca().set_title(title_opt, pad=20)
if gtff.USE_ION:
plt.ion()
+ plt.tight_layout()
+
plt.show()
print("")
-
-
-def parse_from_port_type(
- parser_in: argparse.ArgumentParser, port_type: str, other_args: List[str]
-):
- """
-
- Parameters
- ----------
- parser_in: ArgumentParser
- parser to get data from
- port_type: str
- Type of optimization that will be done. One of ["max_sharpe","min_vol", "eff_risk", "eff_ret"]
- other_args: List[str]
- Arguments passed to function
- Returns
- -------
-
- ns_parser:
- Parsed arguments
- """
-
- if port_type in ["max_sharpe", "min_volatility"]:
- ns_parser = parse_known_args_and_warn(parser_in, other_args)
- if not ns_parser:
- return None
- return ns_parser
-
- elif port_type == "eff_risk":
- parser_in.add_argument(
- "-r", "--risk", type=float, dest="risk_level", default=0.1
- )
- ns_parser = parse_known_args_and_warn(parser_in, other_args)
- if not ns_parser:
- return None
- return ns_parser
-
- elif port_type == "eff_ret":
- parser_in.add_argument(
- "-r", "--return", type=float, dest="target_return", default=0.1
- )
-
- ns_parser = parse_known_args_and_warn(parser_in, other_args)
- if not ns_parser:
- return None
- return ns_parser
- else:
- print("Incorrect portfolio optimizer type\n")
- return None
diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py
index e9d74a76138..6c61666d5bf 100644
--- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py
+++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py
@@ -10,13 +10,17 @@ from pypfopt import plotting
from pypfopt import risk_models
from pypfopt import expected_returns
from pypfopt import EfficientFrontier
-from gamestonk_terminal.helper_funcs import parse_known_args_and_warn, plot_autoscale
+from gamestonk_terminal.helper_funcs import (
+ parse_known_args_and_warn,
+ plot_autoscale,
+ check_non_negative,
+)
from gamestonk_terminal.portfolio_optimization.optimizer_helper import (
process_stocks,
prepare_efficient_frontier,
pie_chart_weights,
display_weights,
- parse_from_port_type,
+ check_valid_property_type,
)
from gamestonk_terminal.config_plot import PLOT_DPI
from gamestonk_terminal import feature_flags as gtff
@@ -35,6 +39,20 @@ period_choices = [
"max",
]
+d_period = {
+ "1d": "[1 Day]",
+ "5d": "[5 Days]",
+ "1mo": "[1 Month]",
+ "3mo": "[3 Months]",
+ "6mo": "[6 Months]",
+ "1y": "[1 Year]",
+ "2y": "[2 Years]",
+ "5y": "[5 Years]",
+ "10y": "[10 Years]",
+ "ytd": "[Year-to-Date]",
+ "max": "[All-time]",
+}
+
def equal_weight(stocks: List[str], other_args: List[str]):
"""
@@ -53,8 +71,8 @@ def equal_weight(stocks: List[str], other_args: List[str]):
"""
parser = argparse.ArgumentParser(
add_help=False,
- prog="equal_weight",
- description="Return equally weighted portfolio holdings",
+ prog="equal",
+ description="Returns an equally weighted portfolio",
)
parser.add_argument(
@@ -76,14 +94,17 @@ def equal_weight(stocks: List[str], other_args: List[str]):
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return
+
+ if len(stocks) < 2:
+ print("Please have at least 2 loaded tickers to calculate weights.\n")
+ return
+
values = {}
- n_stocks = len(stocks)
for stock in stocks:
- values[stock] = ns_parser.value * round(1 / n_stocks, 5)
+ values[stock] = ns_parser.value * round(1 / len(stocks), 5)
if ns_parser.pie:
- pie_chart_weights(values, "equal", 0)
- if n_stocks >= 1:
- print("Equal Weight Portfolio: ")
+ pie_chart_weights(values, "Equally Weighted Portfolio")
+ else:
display_weights(values)
print("")
@@ -92,16 +113,15 @@ def equal_weight(stocks: List[str], other_args: List[str]):
print("")
-def property_weighting(stocks: List[str], property_type: str, other_args: List[str]):
- """
- Property weighted portfolio where each weight is the relative fraction.
+def property_weighting(stocks: List[str], other_args: List[str]):
+ """Weighted portfolio where each weight is the relative fraction of a specified info property.
+
Parameters
----------
stocks: List[str]
List of tickers to be included in optimization
- property_type: str
- Property to weight by. Can be anything in yfinance.Ticker().info. Examples:
- "marketCap", "dividendYield", etc
+ other_args : List[str]
+ Command line arguments to be processed with argparse
Returns
-------
@@ -110,10 +130,32 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s
"""
parser = argparse.ArgumentParser(
add_help=False,
- prog="market_cap_weighted",
- description="Return portfolio weights/values that are weighted by marketcap",
+ prog="property",
+ description="Returns a portfolio that is weighted based on a selected property info",
+ )
+ parser.add_argument(
+ "-p",
+ "--property",
+ required=True if "-h" not in other_args else False,
+ type=check_valid_property_type,
+ dest="property",
+ help="""Property info to weigh. Use one of:
+ previousClose, regularMarketOpen, twoHundredDayAverage, trailingAnnualDividendYield,
+ payoutRatio, volume24Hr, regularMarketDayHigh, navPrice, averageDailyVolume10Day, totalAssets,
+ regularMarketPreviousClose, fiftyDayAverage, trailingAnnualDividendRate, open, toCurrency, averageVolume10days,
+ expireDate, yield, algorithm, dividendRate, exDividendDate, beta, circulatingSupply, regularMarketDayLow,
+ priceHint, currency, trailingPE, regularMarketVolume, lastMarket, maxSupply, openInterest, marketCap,
+ volumeAllCurrencies, strikePrice, averageVolume, priceToSalesTrailing12Months, dayLow, ask, ytdReturn, askSize,
+ volume, fiftyTwoWeekHigh, forwardPE, fromCurrency, fiveYearAvgDividendYield, fiftyTwoWeekLow, bid, dividendYield,
+ bidSize, dayHigh, annualHoldingsTurnover, enterpriseToRevenue, beta3Year, profitMargins, enterpriseToEbitda,
+ 52WeekChange, morningStarRiskRating, forwardEps, revenueQuarterlyGrowth, sharesOutstanding, fundInceptionDate,
+ annualReportExpenseRatio, bookValue, sharesShort, sharesPercentSharesOut, fundFamily, lastFiscalYearEnd,
+ heldPercentInstitutions, netIncomeToCommon, trailingEps, lastDividendValue, SandP52WeekChange, priceToBook,
+ heldPercentInsiders, shortRatio, sharesShortPreviousMonthDate, floatShares, enterpriseValue,
+ threeYearAverageReturn, lastSplitFactor, legalType, lastDividendDate, morningStarOverallRating,
+ earningsQuarterlyGrowth, pegRatio, lastCapGain, shortPercentOfFloat, sharesShortPriorMonth,
+ impliedSharesOutstanding, fiveYearAverageReturn, and regularMarketPrice.""",
)
-
parser.add_argument(
"-v",
"--value",
@@ -129,33 +171,42 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s
default=False,
help="Display a pie chart for weights",
)
- weights = {}
- prop = {}
- prop_sum = 0
try:
+ if other_args:
+ if "-" not in other_args[0]:
+ other_args.insert(0, "-p")
+
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return
+ if len(stocks) < 2:
+ print("Please have at least 2 loaded tickers to calculate weights.\n")
+ return
+
+ weights = {}
+ prop = {}
+ prop_sum = 0
for stock in stocks:
- stock_prop = yf.Ticker(stock).info[property_type]
+ stock_prop = yf.Ticker(stock).info[ns_parser.property]
if stock_prop is None:
stock_prop = 0
prop[stock] = stock_prop
prop_sum += stock_prop
+
+ if prop_sum == 0:
+ print(f"No {ns_parser.property} was found on list of tickers provided")
+ print("")
+ return
+
for k, v in prop.items():
weights[k] = round(v / prop_sum, 5) * ns_parser.value
if ns_parser.pie:
- pie_chart_weights(weights, property_type, 0)
-
- if property_type == "marketCap":
- print("Market Cap Weighted Portfolio: ")
-
- elif property_type == "dividendYield":
- print("Dividend Yield Weighted Portfolio: ")
-
- if len(stocks) >= 1:
+ pie_chart_weights(
+ weights, "Weighted Portfolio based on " + ns_parser.property
+ )
+ else:
display_weights(weights)
print("")
@@ -165,23 +216,23 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s
return
-def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]):
+def max_sharpe(stocks: List[str], other_args: List[str]):
"""
- Return a portfolio based on condition in port_type Defaults to 3m of historical data
+ Return a portfolio that maximises the Sharpe Ratio
+
Parameters
----------
stocks: List[str]
List of the stocks to be included in the weights
- port_type: str
- Method to be used on ef object (example: max_sharpe, min_volatility)
+
Returns
-------
weights: dict
Dictionary of weights where keys are the tickers.
"""
-
- parser = argparse.ArgumentParser(add_help=False, prog=port_type)
-
+ parser = argparse.ArgumentParser(
+ add_help=False, prog="maxsharpe", description="Maximise the Sharpe Ratio"
+ )
parser.add_argument(
"-p",
"--period",
@@ -190,7 +241,100 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]):
help="period to get yfinance data from",
choices=period_choices,
)
+ parser.add_argument(
+ "-v",
+ "--value",
+ dest="value",
+ help="Amount to allocate to portfolio",
+ type=float,
+ default=1.0,
+ )
+ parser.add_argument(
+ "--pie",
+ action="store_true",
+ dest="pie",
+ default=False,
+ help="Display a pie chart for weights",
+ )
+ if other_args:
+ if "-" not in other_args[0]:
+ other_args.insert(0, "-r")
+ parser.add_argument(
+ "-r",
+ "--risk-free-rate",
+ type=float,
+ dest="risk_free_rate",
+ default=0.02,
+ help="""Risk-free rate of borrowing/lending. The period of the risk-free rate
+ should correspond to the frequency of expected returns.""",
+ )
+
+ try:
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return None
+
+ if len(stocks) < 2:
+ print("Please have at least 2 loaded tickers to calculate weights.\n")
+ return
+
+ period = ns_parser.period
+ stock_prices = process_stocks(stocks, period)
+ ef = prepare_efficient_frontier(stock_prices)
+
+ sp = d_period[ns_parser.period]
+
+ ef_opt = dict(ef.max_sharpe(ns_parser.risk_free_rate))
+ s_title = f"{sp} Weights that maximize Sharpe ratio with risk free level of {ns_parser.risk_free_rate}"
+
+ weights = {
+ key: ns_parser.value * round(value, 5) for key, value in ef_opt.items()
+ }
+
+ if ns_parser.pie:
+ pie_chart_weights(weights, s_title)
+ else:
+ print(s_title)
+ display_weights(weights)
+ print("")
+
+ ef.portfolio_performance(verbose=True)
+ print("")
+
+ except Exception as e:
+ print(e)
+ print("")
+ return
+
+
+def min_volatility(stocks: List[str], other_args: List[str]):
+ """
+ Return a portfolio that optimizes for minimum volatility
+
+ Parameters
+ ----------
+ stocks: List[str]
+ List of the stocks to be included in the weights
+
+ Returns
+ -------
+ weights: dict
+ Dictionary of weights where keys are the tickers.
+ """
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog="min_volatility",
+ description="Optimizes for minimum volatility",
+ )
+ parser.add_argument(
+ "-p",
+ "--period",
+ default="3mo",
+ dest="period",
+ help="period to get yfinance data from",
+ choices=period_choices,
+ )
parser.add_argument(
"-v",
"--value",
@@ -208,67 +352,363 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]):
)
try:
- ns_parser = parse_from_port_type(parser, port_type, other_args)
+ ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
- return
+ return None
+
if len(stocks) < 2:
print("Please have at least 2 loaded tickers to calculate weights.\n")
return
+
period = ns_parser.period
stock_prices = process_stocks(stocks, period)
ef = prepare_efficient_frontier(stock_prices)
- if port_type == "max_sharpe":
+ sp = d_period[ns_parser.period]
- ef_sharpe = dict(ef.max_sharpe())
- weights = {
- key: ns_parser.value * round(value, 5)
- for key, value in ef_sharpe.items()
- }
- val = 0
- print("Weights that maximize Sharpe Ratio:")
+ ef_opt = dict(ef.min_volatility())
+ s_title = f"{sp} Weights that minimize volatility"
- elif port_type == "min_volatility":
-
- ef_min_vol = dict(ef.min_volatility())
- weights = {
- key: ns_parser.value * round(value, 5)
- for key, value in ef_min_vol.items()
- }
- val = 0
- print("Weights that minimize volatility")
-
- elif port_type == "eff_risk":
-
- ef_eff_risk = dict(ef.efficient_risk(ns_parser.risk_level))
- weights = {
- key: ns_parser.value * round(value, 5)
- for key, value in ef_eff_risk.items()
- }
- val = ns_parser.risk_level
- print(f"Weights for maximizing returns at risk = {100*val:.1f} %")
-
- elif port_type == "eff_ret":
-
- ef_eff_risk = dict(ef.efficient_return(ns_parser.target_return))
- weights = {
- key: ns_parser.value * round(value, 5)
- for key, value in ef_eff_risk.items()
- }
- val = ns_parser.target_return
- print(f"Weights for minimizing risk at target return = {100*val:.1f} %")
-
- else:
- raise ValueError("EF Method not found")
+ weights = {
+ key: ns_parser.value * round(value, 5) for key, value in ef_opt.items()
+ }
if ns_parser.pie:
- pie_chart_weights(weights, port_type, val)
+ pie_chart_weights(weights, s_title)
+ else:
+ print(s_title)
+ display_weights(weights)
+ print("")
+ ef.portfolio_performance(verbose=True)
+ print("")
+
+ except Exception as e:
+ print(e)
+ print("")
+ return
+
+
+def max_quadratic_utility(stocks: List[str], other_args: List[str]):
+ """
+ Return a portfolio that maximises the quadratic utility, given some risk aversion
+
+ Parameters
+ ----------
+ stocks: List[str]
+ List of the stocks to be included in the weights
+
+ Returns
+ -------
+ weights: dict
+ Dictionary of weights where keys are the tickers.
+ """
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog="max_quadratic_utility",
+ description="Maximises the quadratic utility, given some risk aversion",
+ )
+ parser.add_argument(
+ "-p",
+ "--period",
+ default="3mo",
+ dest="period",
+ help="period to get yfinance data from",
+ choices=period_choices,
+ )
+ parser.add_argument(
+ "-v",
+ "--value",
+ dest="value",
+ help="Amount to allocate to portfolio",
+ type=float,
+ default=1.0,
+ )
+ parser.add_argument(
+ "-n",
+ "--market-neutral",
+ action="store_true",
+ default=False,
+ dest="market_neutral",
+ help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False.
+ Requires negative lower weight bound.""",
+ )
+ if "-n" not in other_args:
+ parser.add_argument(
+ "--pie",
+ action="store_true",
+ dest="pie",
+ default=False,
+ help="Display a pie chart for weights. Only if neutral flag is left False.",
+ )
+
+ if other_args:
+ if "-" not in other_args[0]:
+ other_args.insert(0, "-r")
+
+ parser.add_argument(
+ "-r",
+ "--risk-aversion",
+ type=float,
+ dest="risk_aversion",
+ default=1,
+ help="risk aversion parameter",
+ )
+
+ try:
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return None
+
+ if len(stocks) < 2:
+ print("Please have at least 2 loaded tickers to calculate weights.\n")
+ return
+
+ period = ns_parser.period
+ stock_prices = process_stocks(stocks, period)
+ ef = prepare_efficient_frontier(stock_prices)
+
+ sp = d_period[ns_parser.period]
+
+ ef_opt = dict(
+ ef.max_quadratic_utility(ns_parser.risk_aversion, ns_parser.market_neutral)
+ )
+ s_title = f"{sp} Weights that maximise the quadratic utility with risk aversion of {ns_parser.risk_aversion}"
+
+ weights = {
+ key: ns_parser.value * round(value, 5) for key, value in ef_opt.items()
+ }
+
+ if not ns_parser.market_neutral:
+ if ns_parser.pie:
+ pie_chart_weights(weights, s_title)
+ ef.portfolio_performance(verbose=True)
+ print("")
+ return
+
+ print(s_title)
+ display_weights(weights)
print("")
ef.portfolio_performance(verbose=True)
print("")
+
+ except Exception as e:
+ print(e)
+ print("")
+ return
+
+
+def efficient_risk(stocks: List[str], other_args: List[str]):
+ """
+ Return a portfolio that maximises return for a target risk. The resulting portfolio will have
+ a volatility less than the target (but not guaranteed to be equal)
+
+ Parameters
+ ----------
+ stocks: List[str]
+ List of the stocks to be included in the weights
+
+ Returns
+ -------
+ weights: dict
+ Dictionary of weights where keys are the tickers.
+ """
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog="effrisk",
+ description="""Maximise return for a target risk. The resulting portfolio will have
+ a volatility less than the target (but not guaranteed to be equal)""",
+ )
+ parser.add_argument(
+ "-p",
+ "--period",
+ default="3mo",
+ dest="period",
+ help="period to get yfinance data from",
+ choices=period_choices,
+ )
+ parser.add_argument(
+ "-v",
+ "--value",
+ dest="value",
+ help="Amount to allocate to portfolio",
+ type=float,
+ default=1.0,
+ )
+ parser.add_argument(
+ "-n",
+ "--market-neutral",
+ action="store_true",
+ default=False,
+ dest="market_neutral",
+ help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False.
+ Requires negative lower weight bound.""",
+ )
+ if "-n" not in other_args:
+ parser.add_argument(
+ "--pie",
+ action="store_true",
+ dest="pie",
+ default=False,
+ help="Display a pie chart for weights. Only if neutral flag is left False.",
+ )
+
+ if other_args:
+ if "-" not in other_args[0]:
+ other_args.insert(0, "-t")
+ parser.add_argument(
+ "-t",
+ "--target-volatility",
+ type=float,
+ dest="target_volatility",
+ default=0.1,
+ help="The desired maximum volatility of the resulting portfolio",
+ )
+
+ try:
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return None
+
+ if len(stocks) < 2:
+ print("Please have at least 2 loaded tickers to calculate weights.\n")
+ return
+
+ period = ns_parser.period
+ stock_prices = process_stocks(stocks, period)
+ ef = prepare_efficient_frontier(stock_prices)
+
+ sp = d_period[ns_parser.period]
+
+ ef_opt = dict(
+ ef.efficient_risk(ns_parser.target_volatility, ns_parser.market_neutral)
+ )
+ s_title = f"{sp} Weights that maximize return with a maximum volatility of {ns_parser.target_volatility}"
+
+ weights = {
+ key: ns_parser.value * round(value, 5) for key, value in ef_opt.items()
+ }
+
+ if not ns_parser.market_neutral:
+ if ns_parser.pie:
+ pie_chart_weights(weights, s_title)
+ ef.portfolio_performance(verbose=True)
+ print("")
+ return
+
+ print(s_title)
display_weights(weights)
print("")
+ ef.portfolio_performance(verbose=True)
+ print("")
+
+ except Exception as e:
+ print(e)
+ print("")
+ return
+
+
+def efficient_return(stocks: List[str], other_args: List[str]):
+ """
+ Return a portfolio that minimises volatility for a given target return ('Markowitz portfolio')
+
+ Parameters
+ ----------
+ stocks: List[str]
+ List of the stocks to be included in the weights
+
+ Returns
+ -------
+ weights: dict
+ Dictionary of weights where keys are the tickers.
+ """
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog="effret",
+ description="Calculate the 'Markowitz portfolio', minimising volatility for a given target return",
+ )
+ parser.add_argument(
+ "-p",
+ "--period",
+ default="3mo",
+ dest="period",
+ help="period to get yfinance data from",
+ choices=period_choices,
+ )
+ parser.add_argument(
+ "-v",
+ "--value",
+ dest="value",
+ help="Amount to allocate to portfolio",
+ type=float,
+ default=1.0,
+ )
+ parser.add_argument(
+ "-n",
+ "--market-neutral",
+ action="store_true",
+ default=False,
+ dest="market_neutral",
+ help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False.
+ Requires negative lower weight bound.""",
+ )
+ if "-n" not in other_args:
+ parser.add_argument(
+ "--pie",
+ action="store_true",
+ dest="pie",
+ default=False,
+ help="Display a pie chart for weights. Only if neutral flag is left False.",
+ )
+
+ if other_args:
+ if "-" not in other_args[0]:
+ other_args.insert(0, "-t")
+ parser.add_argument(
+ "-t",
+ "--target-return",
+ type=float,
+ dest="target_return",
+ default=0.1,
+ help="the desired return of the resulting portfolio",
+ )
+
+ try:
+ ns_parser = parse_known_args_and_warn(parser, other_args)
+ if not ns_parser:
+ return None
+
+ if len(stocks) < 2:
+ print("Please have at least 2 loaded tickers to calculate weights.\n")
+ return
+
+ period = ns_parser.period
+ stock_prices = process_stocks(stocks, period)
+ ef = prepare_efficient_frontier(stock_prices)
+
+ sp = d_period[ns_parser.period]
+
+ ef_opt = dict(
+ ef.efficient_return(ns_parser.target_return, ns_parser.market_neutral)
+ )
+ s_title = f"{sp} Weights that minimise volatility for a given target return of {ns_parser.target_return}"
+
+ weights = {
+ key: ns_parser.value * round(value, 5) for key, value in ef_opt.items()
+ }
+
+ if not ns_parser.market_neutral:
+ if ns_parser.pie:
+ pie_chart_weights(weights, s_title)
+ ef.portfolio_performance(verbose=True)
+ print("")
+ return
+
+ print(s_title)
+ display_weights(weights)
+ print("")
+ ef.portfolio_performance(verbose=True)
+ print("")
except Exception as e:
print(e)
@@ -277,7 +717,12 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]):
def show_ef(stocks: List[str], other_args: List[str]):
- parser = argparse.ArgumentParser(add_help=False, prog="ef")
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ prog="ef",
+ description="""This function plots random portfolios based
+ on their risk and returns and shows the efficient frontier.""",
+ )
parser.add_argument(
"-p",
@@ -288,10 +733,18 @@ def show_ef(stocks: List[str], other_args: List[str]):
choices=period_choices,
)
parser.add_argument(
- "-n", default=300, dest="n_port", help="number of portfolios to simulate"
+ "-n",
+ "--number-portfolios",
+ default=300,
+ type=check_non_negative,
+ dest="n_port",
+ help="number of portfolios to simulate",
)
try:
+ if other_args:
+ if "-" not in other_args[0]:
+ other_args.insert(0, "-n")
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return
@@ -319,7 +772,7 @@ def show_ef(stocks: List[str], other_args: List[str]):
ret_sharpe, std_sharpe, _ = ef.portfolio_performance()
ax.scatter(std_sharpe, ret_sharpe, marker="*", s=100, c="r", label="Max Sharpe")
- ax.set_title("Efficient Frontier")
+ ax.set_title(f"Efficient Frontier simulating {ns_parser.n_port} portfolios")
ax.legend()
plt.tight_layout()
plt.grid(b=True, which="major", color="#666666", linestyle="-")
diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py
index df2f8a55fa6..c66ca63e846 100644
--- a/gamestonk_terminal/portfolio_optimization/po_controller.py
+++ b/gamestonk_terminal/portfolio_optimization/po_controller.py
@@ -9,7 +9,7 @@ from prompt_toolkit.completion import NestedCompleter
from gamestonk_terminal import feature_flags as gtff
from gamestonk_terminal.helper_funcs import get_flair, parse_known_args_and_warn
from gamestonk_terminal.menu import session
-from gamestonk_terminal.portfolio_optimization import optimizer_view as po_api
+from gamestonk_terminal.portfolio_optimization import optimizer_view
class PortfolioOptimization:
@@ -20,14 +20,17 @@ class PortfolioOptimization:
"quit",
"select",
"add",
- "equal_weight",
- "mkt_cap",
- "div_yield",
- "max_sharpe",
- "min_vol",
- "eff_risk",
- "eff_ret",
- "show_ef",
+ "equal",
+ "mktcap",
+ "dividend",
+ "property",
+ "maxsharpe",
+ "minvol",
+ "effret",
+ "effrisk",
+ "maxquadutil",
+ "ef",
+ "ca",
"yolo",
]
@@ -52,21 +55,33 @@ class PortfolioOptimization:
print(" quit quit to abandon program")
print(f"\nCurrent Tickers: {('None', ', '.join(tickers))[bool(tickers)]}")
print("")
- print(" add add ticker to optimize")
- print(" select overwrite current tickers with new tickers")
+ print(" add add ticker to optimize")
+ print(" select overwrite current tickers with new tickers")
print("")
print("Optimization:")
+ print(" equal equally weighted")
+ print(" mktcap weighted according to market cap (property marketCap)")
+ print(
+ " dividend weighted according to dividend yield (property dividendYield)"
+ )
+ print(" property weight according to selected info property")
print("")
- print(" equal_weight equally weighted portfolio")
- print(" mkt_cap marketcap weighted portfolio")
- print(" div_yield dividend weighted portfolio")
- print(" max_sharpe portfolio with maximum sharpe ratio")
- print(" min_vol portfolio with minimum volatility")
- print(" eff_risk portfolio that maximizes returns at given risk")
- print(" eff_ret portfolio that minimizes risk at given return")
- print(" show_ef show the efficient frontier")
+ print("Mean Variance Optimization:")
+ print(
+ " maxsharpe optimizes for maximal Sharpe ratio (a.k.a the tangency portfolio)"
+ )
+ print(" minvol optimizes for minimum volatility")
+ print(
+ " maxquadutil maximises the quadratic utility, given some risk aversion"
+ )
+ print(" effret maximises return for a given target risk")
+ print(" effrisk minimises risk for a given target return")
print("")
- plt.close("all")
+ print(" ef show the efficient frontier")
+ print("")
+ if tickers:
+ print(" > ca comparison analysis for selected tickers")
+ print("")
def switch(self, an_input: str):
"""Process and dispatch input
@@ -97,36 +112,55 @@ class PortfolioOptimization:
return True
def call_add(self, other_args: List[str]):
+ """Process add command"""
self.add_stocks(self, other_args)
def call_select(self, other_args: List[str]):
+ """Process select command"""
self.tickers = []
self.add_stocks(self, other_args)
- def call_equal_weight(self, other_args: List[str]):
- po_api.equal_weight(self.tickers, other_args)
+ def call_equal(self, other_args: List[str]):
+ """Process equal command"""
+ optimizer_view.equal_weight(self.tickers, other_args)
- def call_mkt_cap(self, other_args: List[str]):
- po_api.property_weighting(self.tickers, "marketCap", other_args)
+ def call_mktcap(self, other_args: List[str]):
+ """Process mktcap command"""
+ other_args.insert(0, "marketCap")
+ optimizer_view.property_weighting(self.tickers, other_args)
- def call_div_yield(self, other_args: List[str]):
- po_api.property_weighting(self.tickers, "dividendYield", other_args)
+ def call_dividend(self, other_args: List[str]):
+ """Process dividend command"""
+ other_args.insert(0, "dividendYield")
+ optimizer_view.property_weighting(self.tickers, other_args)
- def call_max_sharpe(self, other_args: List[str]):
- po_api.ef_portfolio(self.tickers, "max_sharpe", other_args)
+ def call_property(self, other_args: List[str]):
+ """Process property command"""
+ optimizer_view.property_weighting(self.tickers, other_args)
- def call_min_vol(self, other_args: List[str]):
- po_api.ef_portfolio(self.tickers, "min_volatility", other_args)
+ def call_maxsharpe(self, other_args: List[str]):
+ """Process maxsharpe command"""
+ optimizer_view.max_sharpe(self.tickers, other_args)
- def call_eff_risk(self, other_args: List[str]):
- po_api.ef_portfolio(self.tickers, "eff_risk", other_args)
+ def call_minvol(self, other_args: List[str]):
+ """Process minvol command"""
+ optimizer_view.min_volatility(self.tickers, other_args)
- def call_eff_ret(self, other_args: List[str]):
- po_api.ef_portfolio(self.tickers, "eff_ret", other_args)
+ def call_maxquadutil(self, other_args: List[str]):
+ """Process maxquadutil command"""
+ optimizer_view.max_quadratic_utility(self.tickers, other_args)
- def call_show_ef(self, other_args):
- po_api.show_ef(self.tickers, other_args)
- print("")
+ def call_effrisk(self, other_args: List[str]):
+ """Process effrisk command"""
+ optimizer_view.efficient_risk(self.tickers, other_args)
+
+ def call_effret(self, other_args: List[str]):
+ """Process effret command"""
+ optimizer_view.efficient_return(self.tickers, other_args)
+
+ def call_ef(self, other_args):
+ """Process ef command"""
+ optimizer_view.show_ef(self.tickers, other_args)
def call_yolo(self, _):
# Easter egg :)
@@ -136,11 +170,11 @@ class PortfolioOptimization:
@staticmethod
def add_stocks(self, other_args: List[str]):
- """ Add ticker to current list for optimization"""
+ """ Add ticker or Select tickes for portfolio to be optimized """
parser = argparse.ArgumentParser(
add_help=False,
- prog="add",
- description="""Add tickers for optimizing.""",
+ prog="add/select",
+ description="""Add/Select tickers for portfolio to be optimized.""",
)
parser.add_argument(
"-t",
@@ -148,10 +182,9 @@ class PortfolioOptimization:
dest="add_tickers",
type=lambda s: [str(item).upper() for item in s.split(",")],
default=[],
- help="add tickers to optimzation.",
+ help="tickers to be used in the portfolio to optimize.",
)
try:
-
if other_args:
if "-" not in other_args[0]:
other_args.insert(0, "-t")
@@ -162,12 +195,14 @@ class PortfolioOptimization:
tickers = set(self.tickers)
for ticker in ns_parser.add_tickers:
tickers.add(ticker)
+
+ if self.tickers:
+ print(
+ f"\nCurrent Tickers: {('None', ', '.join(tickers))[bool(tickers)]}"
+ )
+
self.tickers = list(tickers)
- print(
- f"\nCurrent Tickers: {('None', ', '.join(self.tickers))[bool(self.tickers)]}"
- )
- print("")
except Exception as e:
print(e)
@@ -196,6 +231,8 @@ def menu(tickers: List[str]):
an_input = input(f"{get_flair()} (po)> ")
try:
+ plt.close("all")
+
process_input = po_controller.switch(an_input)
if process_input is not None:
diff --git a/gamestonk_terminal/screener/README.md b/gamestonk_terminal/screener/README.md
index 6271a7684db..3135fb3f981 100644
--- a/gamestonk_terminal/screener/README.md
+++ b/gamestonk_terminal/screener/README.md
@@ -23,8 +23,8 @@ This menu aims to filter stocks based on pre-specified preset filters, and the u
* contains [Oversold (-s) signal example](#signal-oversold)
* [signals](#signals)
* view filter signals (e.g. -s top_gainers) [Finviz]
-* [po](#port_opt)
- * go to the portfolio optimization menu
+* [> po](portfolio_optimization/README.md)
+ * portfolio optimization for last screened tickers
## view
@@ -190,4 +190,4 @@ usage: po ticker1 ticker2 ...
````
example: po aapl msft tsla gme
-````
\ No newline at end of file
+````
diff --git a/gamestonk_terminal/screener/finviz_view.py b/gamestonk_terminal/screener/finviz_view.py
index 4ca3bb96857..3c331be9649 100644
--- a/gamestonk_terminal/screener/finviz_view.py
+++ b/gamestonk_terminal/screener/finviz_view.py
@@ -214,12 +214,16 @@ def screener(other_args: List[str], loaded_preset: str, data_type: str):
if isinstance(df_screen, pd.DataFrame):
print(df_screen.to_string())
- print("")
+ print("")
+ return list(df_screen["Ticker"].values)
+ else:
+ print("")
+ return []
except Exception as e:
print(e)
print("")
- return
+ return []
def view_signals(other_args: List[str]):
diff --git a/gamestonk_terminal/screener/screener_controller.py b/gamestonk_terminal/screener/screener_controller.py
index 0fe364750b0..7edc438c501 100644
--- a/gamestonk_terminal/screener/screener_controller.py
+++ b/gamestonk_terminal/screener/screener_controller.py
@@ -41,6 +41,7 @@ class ScreenerController:
def __init__(self):
"""Constructor"""
self.preset = "template"
+ self.screen_tickers = []
self.scr_parser = argparse.ArgumentParser(add_help=False, prog="scr")
self.scr_parser.add_argument(
"cmd",
@@ -71,8 +72,11 @@ class ScreenerController:
print("")
print(" signals view filter signals (e.g. -s top_gainers)")
print("")
- print(" > po go to the portfolio optimization menu")
- print("")
+ if self.screen_tickers:
+ print(f"Last screened tickers: {', '.join(self.screen_tickers)}")
+ print("")
+ print(" > po portfolio optimization for last screened tickers")
+ print("")
@staticmethod
def view_available_presets(other_args: List[str]):
@@ -191,39 +195,41 @@ class ScreenerController:
def call_historical(self, other_args: List[str]):
"""Process historical command"""
- yahoo_finance_view.historical(other_args, self.preset)
+ self.screen_tickers = yahoo_finance_view.historical(other_args, self.preset)
def call_overview(self, other_args: List[str]):
"""Process overview command"""
- finviz_view.screener(other_args, self.preset, "overview")
+ self.screen_tickers = finviz_view.screener(other_args, self.preset, "overview")
def call_valuation(self, other_args: List[str]):
"""Process valuation command"""
- finviz_view.screener(other_args, self.preset, "valuation")
+ self.screen_tickers = finviz_view.screener(other_args, self.preset, "valuation")
def call_financial(self, other_args: List[str]):
"""Process financial command"""
- finviz_view.screener(other_args, self.preset, "financial")
+ self.screen_tickers = finviz_view.screener(other_args, self.preset, "financial")
def call_ownership(self, other_args: List[str]):
"""Process ownership command"""
- finviz_view.screener(other_args, self.preset, "ownership")
+ self.screen_tickers = finviz_view.screener(other_args, self.preset, "ownership")
def call_performance(self, other_args: List[str]):
"""Process performance command"""
- finviz_view.screener(other_args, self.preset, "performance")
+ self.screen_tickers = finviz_view.screener(
+ other_args, self.preset, "performance"
+ )
def call_technical(self, other_args: List[str]):
"""Process technical command"""
- finviz_view.screener(other_args, self.preset, "technical")
+ self.screen_tickers = finviz_view.screener(other_args, self.preset, "technical")
def call_signals(self, other_args: List[str]):
"""Process signals command"""
finviz_view.view_signals(other_args)
def call_po(self, other_args: List[str]):
- """Call the portfolio optimization menu"""
- return po_controller.menu(other_args)
+ """Call the portfolio optimization menu with selected tickers"""
+ return po_controller.menu(self.screen_tickers)
def menu():
diff --git a/gamestonk_terminal/screener/yahoo_finance_view.py b/gamestonk_terminal/screener/yahoo_finance_view.py
index 6ad1c6e93bb..2a05ddbeec1 100644
--- a/gamestonk_terminal/screener/yahoo_finance_view.py
+++ b/gamestonk_terminal/screener/yahoo_finance_view.py
@@ -1,5 +1,6 @@
import argparse
from typing import List
+import random
from pandas.plotting import register_matplotlib_converters
import matplotlib.pyplot as plt
import datetime
@@ -119,10 +120,12 @@ def historical(other_args: List[str], preset_loaded: str):
if len(l_stocks) > 10:
print(
- "We limit stocks shown to 10, as after this the plot becomes too noisy. "
- "Also, we ran out of different colors."
+ "\nThe limit of stocks to compare with are 10. Hence, 10 random similar stocks will be displayed.",
+ "\nThe selected list will be:",
)
- l_stocks = l_stocks[:10]
+ random.shuffle(l_stocks)
+ l_stocks = sorted(l_stocks[:10])
+ print(", ".join(l_stocks))
while l_stocks:
l_parsed_stocks = list()
@@ -179,8 +182,11 @@ def historical(other_args: List[str], preset_loaded: str):
plt.show()
print("")
+ return l_parsed_stocks
except SystemExit:
print("Similar companies need to be provided", "\n")
+ return []
except Exception as e:
print(e, "\n")
+ return []
diff --git a/gamestonk_terminal/technical_analysis/README.md b/gamestonk_terminal/technical_analysis/README.md
index c35ebb6bdaa..0e5c511d850 100644
--- a/gamestonk_terminal/technical_analysis/README.md
+++ b/gamestonk_terminal/technical_analysis/README.md
@@ -43,7 +43,7 @@ This menu aims to perform a technical analysis on a pre-loaded ticker chart, and
* [obv](#obv)
- on balance volume
-**S/O to https://github.com/twopirllc/pandas-ta** _Owing to this library, it is fairly easy to add other technical indicators. So, let me know if there's any that you would like. Personally I use mostly these ones, hence why I didn't add more._
+**S/O to https://github.com/twopirllc/pandas-ta** _Owing to this library, it is fairly easy to add other technical indicators. So, let us know if there's any other that you would like._
## view
```