From 33a4b64b7e3129104b4c913ce70f7b03e9b17aab Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Thu, 15 Apr 2021 14:43:02 +0200 Subject: [PATCH 01/37] add market_news and news_article to disc menu market_news latest news [SeekingAlpha] news_article news article [SeekingAlpha] --- .../discovery/disc_controller.py | 12 +++ .../discovery/seeking_alpha_model.py | 93 +++++++++++++++++++ .../discovery/seeking_alpha_view.py | 70 ++++++++++++++ 3 files changed, 175 insertions(+) diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index 07bb33a3302..fa0f893232f 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -46,6 +46,8 @@ class DiscoveryController: "valuation", "performance", "spectrum", + "market_news", + "news_article" ] def __init__(self): @@ -87,6 +89,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(" market_news latest news [SeekingAlpha]") + print(" news_article news article [SeekingAlpha]") print("") def switch(self, an_input: str): @@ -187,6 +191,14 @@ class DiscoveryController: other_args, "spectrum" ) + def call_market_news(self, other_args: List[str]): + """Process market_news command""" + seeking_alpha_view.articles_list_view(other_args) + + def call_news_article(self, other_args: List[str]): + """Process news_article command""" + seeking_alpha_view.news_article_view(other_args) + def menu(): """Discovery Menu""" diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index 59f63e44ae6..06ec76e8b15 100755 --- a/gamestonk_terminal/discovery/seeking_alpha_model.py +++ b/gamestonk_terminal/discovery/seeking_alpha_model.py @@ -67,3 +67,96 @@ 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(pages: int) -> list: + """Returns a list of recent articles + + Parameters + ---------- + pages : int + Number of pages + + Returns + ------- + list + Latest articles list + """ + + articles = list() + url_articles = "https://seekingalpha.com/market-news" + for idx in range(0, pages): + 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, + 'date': item_row['data-last-date'], + 'url': "https://seekingalpha.com"+article_url, + 'id': article_id + }) + + url_articles = ( + f"https://seekingalpha.com/market-news/{idx+1}" + ) + + 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'], + 'date': jdata['data']['attributes']['lastModified'], + 'content': content + } + + return article diff --git a/gamestonk_terminal/discovery/seeking_alpha_view.py b/gamestonk_terminal/discovery/seeking_alpha_view.py index 2965bf79cce..f54714b11cc 100644 --- a/gamestonk_terminal/discovery/seeking_alpha_view.py +++ b/gamestonk_terminal/discovery/seeking_alpha_view.py @@ -65,3 +65,73 @@ def earnings_release_dates_view(other_args: List[str]): ].to_string(index=False, header=False) ) print("") + + +def articles_list_view(other_args: List[str]): + """Prints a news article list + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-p", "20", "-n", "5"] + """ + + parser = argparse.ArgumentParser( + add_help=False, + prog="up_articles", + description="""Latest articles. [Source: Seeking Alpha]""", + ) + + parser.add_argument( + "-p", + "--pages", + action="store", + dest="n_pages", + type=check_positive, + default=1, + help="Number of pages from Seeking Alpha website.", + ) + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + articles = seeking_alpha_model.get_article_list(ns_parser.n_pages) + for article in articles: + print(article['id'], '-', article['title'], '(', article['url'], ')') + + +def news_article_view(other_args: List[str]): + """Prints a news article + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-i", "3681944"] + """ + + parser = argparse.ArgumentParser( + add_help=False, + prog="up_article", + description="""News article. [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.", + ) + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + if ns_parser.n_id == -1: + return + + article = seeking_alpha_model.get_article_data(ns_parser.n_id) + print(article['date'], article['title']) + print(article['content']) From 4f9669684b1053462dcf4de47f237ea64f354078 Mon Sep 17 00:00:00 2001 From: didier Date: Thu, 15 Apr 2021 22:12:45 +0100 Subject: [PATCH 02/37] Improve portfolio optimization user experience - pedantic stuff --- .../optimizer_helper.py | 57 ++++++----- .../portfolio_optimization/optimizer_view.py | 23 ++--- .../portfolio_optimization/po_controller.py | 95 +++++++++++-------- 3 files changed, 98 insertions(+), 77 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py index 3ca81f396a2..24db45eed91 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py @@ -27,7 +27,7 @@ title_maps = { 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 +51,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 +82,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): @@ -107,7 +107,6 @@ def pie_chart_weights(weights: dict, optimizer: str, value: float): optimzer: str Optimization technique used for title """ - plt.close("all") if not weights: return @@ -122,31 +121,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,24 +151,41 @@ 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)) + plt.gca().set_title(title_maps[optimizer].format(100 * value), pad=20) else: - plt.title(title_maps[optimizer]) + plt.gca().set_title(title_maps[optimizer], 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 index e9d74a76138..6e779b0669a 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -76,14 +76,15 @@ 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: ") + else: display_weights(values) print("") @@ -137,6 +138,9 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s 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 for stock in stocks: stock_prop = yf.Ticker(stock).info[property_type] if stock_prop is None: @@ -148,14 +152,7 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s 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: + else: display_weights(weights) print("") @@ -296,7 +293,7 @@ def show_ef(stocks: List[str], other_args: List[str]): if not ns_parser: return if len(stocks) < 2: - print("Please have at least 2 loaded tickers to calculate weights.\n") + print("Please have at least 2 loaded tickers to calculate weights.") return stock_prices = process_stocks(stocks, ns_parser.period) diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index df2f8a55fa6..d4d2960adee 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,14 @@ class PortfolioOptimization: "quit", "select", "add", - "equal_weight", - "mkt_cap", - "div_yield", - "max_sharpe", - "min_vol", - "eff_risk", - "eff_ret", - "show_ef", + "equal", + "mktcap", + "dividend", + "maxsharpe", + "minvol", + "maxret", + "minrisk", + "ef", "yolo", ] @@ -52,21 +52,19 @@ 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 market cap weighted") + print(" dividend dividend weighted") + print(" maxsharpe maximum sharpe ratio") + print(" minvol minimum volatility") + print(" maxret maximizes returns at given risk") + print(" minrisk minimizes risk at given return") + print(" ef show the efficient frontier") print("") - print(" equal_weight equally weighted portfolio") - print(" mkt_cap marketcap weighted portfolio") - print(" div_yield dividend weighted portfolio") - print(" max_sharpe portfolio with maximum sharpe ratio") - print(" min_vol portfolio with minimum volatility") - print(" eff_risk portfolio that maximizes returns at given risk") - print(" eff_ret portfolio that minimizes risk at given return") - print(" show_ef show the efficient frontier") - print("") - plt.close("all") def switch(self, an_input: str): """Process and dispatch input @@ -97,36 +95,45 @@ 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""" + optimizer_view.property_weighting(self.tickers, "marketCap", other_args) - def call_div_yield(self, other_args: List[str]): - po_api.property_weighting(self.tickers, "dividendYield", other_args) + def call_dividend(self, other_args: List[str]): + """Process dividend command""" + optimizer_view.property_weighting(self.tickers, "dividendYield", other_args) - def call_max_sharpe(self, other_args: List[str]): - po_api.ef_portfolio(self.tickers, "max_sharpe", other_args) + def call_maxsharpe(self, other_args: List[str]): + """Process maxsharpe command""" + optimizer_view.ef_portfolio(self.tickers, "max_sharpe", other_args) - def call_min_vol(self, other_args: List[str]): - po_api.ef_portfolio(self.tickers, "min_volatility", other_args) + def call_minvol(self, other_args: List[str]): + """Process minvol command""" + optimizer_view.ef_portfolio(self.tickers, "min_volatility", other_args) - def call_eff_risk(self, other_args: List[str]): - po_api.ef_portfolio(self.tickers, "eff_risk", other_args) + def call_maxret(self, other_args: List[str]): + """Process maxret command""" + optimizer_view.ef_portfolio(self.tickers, "eff_risk", other_args) - def call_eff_ret(self, other_args: List[str]): - po_api.ef_portfolio(self.tickers, "eff_ret", other_args) + def call_minrisk(self, other_args: List[str]): + """Process minrisk command""" + optimizer_view.ef_portfolio(self.tickers, "eff_ret", other_args) - def call_show_ef(self, other_args): - po_api.show_ef(self.tickers, other_args) - print("") + def call_ef(self, other_args): + """Process ef command""" + optimizer_view.show_ef(self.tickers, other_args) def call_yolo(self, _): # Easter egg :) @@ -162,12 +169,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 +205,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: From 57d56b8b1e2903fada239270b2c3cb59f5ea3e5d Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 00:38:14 +0100 Subject: [PATCH 03/37] some more minor refactoring cuz im picky --- gamestonk_terminal/helper_funcs.py | 2 +- .../optimizer_helper.py | 176 +++++++----- .../portfolio_optimization/optimizer_view.py | 256 +++++++++++++----- .../portfolio_optimization/po_controller.py | 55 ++-- 4 files changed, 335 insertions(+), 154 deletions(-) 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/portfolio_optimization/optimizer_helper.py b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py index 24db45eed91..19f9057bd76 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py @@ -15,15 +15,107 @@ from gamestonk_terminal.config_plot import PLOT_DPI from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import plot_autoscale, parse_known_args_and_warn -title_maps = { - "max_sharpe": "Maximum Sharpe Portfolio", - "min_volatility": "Minimum Volatility Portfolio", - "eff_risk": "Maximum Return Portfolio at Risk = {:1f} %", - "eff_ret": "Minimum Volatility Portfolio at Target Return = {:.1f} %", - "equal": "Equally Weighted Portfolio", - "marketCap": "MarketCap Weighted Portfolio", - "dividendYield": "Dividend Yield Weighted Portfolio", -} +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: @@ -97,15 +189,15 @@ 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 """ if not weights: return @@ -176,10 +268,7 @@ def pie_chart_weights(weights: dict, optimizer: str, value: float): plt.setp(autotexts, size=8, weight="bold") - if optimizer in ["eff_ret", "eff_risk"]: - plt.gca().set_title(title_maps[optimizer].format(100 * value), pad=20) - else: - plt.gca().set_title(title_maps[optimizer], pad=20) + plt.gca().set_title(title_opt, pad=20) if gtff.USE_ION: plt.ion() @@ -188,52 +277,3 @@ def pie_chart_weights(weights: dict, optimizer: str, value: float): 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 6e779b0669a..4f7170e1fd8 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -16,7 +16,7 @@ from gamestonk_terminal.portfolio_optimization.optimizer_helper import ( 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 +35,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 +67,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( @@ -83,7 +97,7 @@ def equal_weight(stocks: List[str], other_args: List[str]): for stock in stocks: values[stock] = ns_parser.value * round(1 / len(stocks), 5) if ns_parser.pie: - pie_chart_weights(values, "equal", 0) + pie_chart_weights(values, "Equally Weighted Portfolio") else: display_weights(values) print("") @@ -93,16 +107,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 ------- @@ -111,10 +124,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, + 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", @@ -130,19 +165,24 @@ 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 @@ -151,7 +191,9 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s weights[k] = round(v / prop_sum, 5) * ns_parser.value if ns_parser.pie: - pie_chart_weights(weights, property_type, 0) + pie_chart_weights( + weights, "Weighted Portfolio based on " + ns_parser.property + ) else: display_weights(weights) print("") @@ -165,20 +207,37 @@ def property_weighting(stocks: List[str], property_type: str, other_args: List[s def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): """ Return a portfolio based on condition in port_type Defaults to 3m of historical data + Parameters ---------- stocks: List[str] List of the stocks to be included in the weights port_type: str - Method to be used on ef object (example: max_sharpe, min_volatility) + Method to be used on ef object (example: max_sharpe, min_volatility, eff_risk, eff_ret) + Returns ------- weights: dict Dictionary of weights where keys are the tickers. """ - parser = argparse.ArgumentParser(add_help=False, prog=port_type) + d_descriptions = { + "max_sharpe": """Maximise the Sharpe Ratio. The result is also referred to as the tangency portfolio, + as it is the portfolio for which the capital market line is tangent to the efficient frontier. + This is a convex optimization problem after making a certain variable substitution. See + Cornuejols and Tutuncu (2006) for more.""", + "min_volatility": "optimizes for minimum volatility", + "max_quadratic_utility": "maximises the quadratic utility, given some risk aversion", + "efficient_risk": """Maximise return for a target risk. The resulting portfolio will have + a volatility less than the target (but not guaranteed to be equal).""", + "efficient_return": "Calculate the 'Markowitz portfolio', minimising volatility for a given target return.", + "max_quadratic_utility": "Maximise the given quadratic utility, i.e: max_w w^T \mu - \frac \delta 2 w^T \Sigma w", + } + + parser = argparse.ArgumentParser( + add_help=False, prog=port_type, description=d_descriptions[port_type] + ) parser.add_argument( "-p", "--period", @@ -187,7 +246,6 @@ 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", @@ -204,68 +262,144 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): help="Display a pie chart for weights", ) + if port_type == "max_sharpe": + 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.""", + ) + + elif port_type == "efficient_risk": + 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", + ) + 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.""", + ) + + elif port_type == "efficient_return": + 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", + ) + 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.""", + ) + + elif port_type == "max_quadratic_utility": + 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", + ) + 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.""", + ) + 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:") + if port_type == "max_sharpe": + ef_opt = dict(ef.max_sharpe(ns_parser.risk_free_rate)) + s_title = f"{sp} Weights that maximize Sharpe ration with risk free level of {ns_parser.risk_free_rate}" elif port_type == "min_volatility": + ef_opt = dict(ef.min_volatility()) + s_title = f"{sp} Weights that minimize 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 == "efficient_risk": + 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}" - elif port_type == "eff_risk": + elif port_type == "efficient_return": + 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}" - 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} %") + elif port_type == "max_quadratic_utility": + 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}" else: raise ValueError("EF Method not found") - if ns_parser.pie: - pie_chart_weights(weights, port_type, val) + 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("") - print("") ef.portfolio_performance(verbose=True) print("") - display_weights(weights) - print("") except Exception as e: print(e) diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index d4d2960adee..24a93bcdd5e 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -21,12 +21,12 @@ class PortfolioOptimization: "select", "add", "equal", - "mktcap", - "dividend", + "property", "maxsharpe", "minvol", - "maxret", - "minrisk", + "effret", + "effrisk", + "maxquadutil", "ef", "yolo", ] @@ -57,12 +57,20 @@ class PortfolioOptimization: print("") print("Optimization:") print(" equal equally weighted") - print(" mktcap market cap weighted") - print(" dividend dividend weighted") - print(" maxsharpe maximum sharpe ratio") - print(" minvol minimum volatility") - print(" maxret maximizes returns at given risk") - print(" minrisk minimizes risk at given return") + print( + " property weight according to selected info property (e.g. marketCap)" + ) + print("") + 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("") @@ -107,13 +115,9 @@ class PortfolioOptimization: """Process equal command""" optimizer_view.equal_weight(self.tickers, other_args) - def call_mktcap(self, other_args: List[str]): - """Process mktcap command""" - optimizer_view.property_weighting(self.tickers, "marketCap", other_args) - - def call_dividend(self, other_args: List[str]): - """Process dividend command""" - optimizer_view.property_weighting(self.tickers, "dividendYield", 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""" @@ -123,13 +127,17 @@ class PortfolioOptimization: """Process minvol command""" optimizer_view.ef_portfolio(self.tickers, "min_volatility", other_args) - def call_maxret(self, other_args: List[str]): - """Process maxret command""" - optimizer_view.ef_portfolio(self.tickers, "eff_risk", other_args) + def call_maxquadutil(self, other_args: List[str]): + """Process maxquadutil command""" + optimizer_view.ef_portfolio(self.tickers, "max_quadratic_utility", other_args) - def call_minrisk(self, other_args: List[str]): - """Process minrisk command""" - optimizer_view.ef_portfolio(self.tickers, "eff_ret", other_args) + def call_effrisk(self, other_args: List[str]): + """Process effrisk command""" + optimizer_view.ef_portfolio(self.tickers, "efficient_risk", other_args) + + def call_effret(self, other_args: List[str]): + """Process effret command""" + optimizer_view.ef_portfolio(self.tickers, "efficient_return", other_args) def call_ef(self, other_args): """Process ef command""" @@ -158,7 +166,6 @@ class PortfolioOptimization: help="add tickers to optimzation.", ) try: - if other_args: if "-" not in other_args[0]: other_args.insert(0, "-t") From b19f0740b57bedd1eb9e172392ef95194910ee8c Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 00:59:42 +0100 Subject: [PATCH 04/37] more refactoring and stuff --- .../comparison_analysis/ca_controller.py | 2 +- gamestonk_terminal/screener/finviz_view.py | 8 ++++-- .../screener/screener_controller.py | 28 +++++++++++-------- .../screener/yahoo_finance_view.py | 12 ++++++-- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py index f999ed092af..e0a339202c2 100644 --- a/gamestonk_terminal/comparison_analysis/ca_controller.py +++ b/gamestonk_terminal/comparison_analysis/ca_controller.py @@ -320,7 +320,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/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 [] From 63e75010666e21dfc1523046f3a10e5cab3059d3 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 01:07:13 +0100 Subject: [PATCH 05/37] Update README.md --- gamestonk_terminal/README.md | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 2b6d79c7b2e..e616a2ae873 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-) @@ -242,16 +243,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   @@ -331,7 +335,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/)   @@ -351,4 +357,4 @@ technical |technical (e.g. Beta, SMA50, 52W Low, RSI, Change) signals |view filter signals (e.g. -s top_gainers) -  \ No newline at end of file +  From 1c04f02da4b0e295fc4d7483eb884d72c0c896ee Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 01:10:56 +0100 Subject: [PATCH 06/37] fix linting --- gamestonk_terminal/portfolio_optimization/optimizer_helper.py | 2 +- gamestonk_terminal/portfolio_optimization/optimizer_view.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py index 19f9057bd76..f88ad80463c 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py @@ -13,7 +13,7 @@ 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 l_valid_property_infos = [ "previousClose", diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py index 4f7170e1fd8..92ee5417fb9 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -232,7 +232,6 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): "efficient_risk": """Maximise return for a target risk. The resulting portfolio will have a volatility less than the target (but not guaranteed to be equal).""", "efficient_return": "Calculate the 'Markowitz portfolio', minimising volatility for a given target return.", - "max_quadratic_utility": "Maximise the given quadratic utility, i.e: max_w w^T \mu - \frac \delta 2 w^T \Sigma w", } parser = argparse.ArgumentParser( From 446de338b05113ac401900d979f39a8816dfacf4 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 01:31:25 +0100 Subject: [PATCH 07/37] Update README.md --- .../portfolio_optimization/README.md | 95 +++++++++++++------ 1 file changed, 68 insertions(+), 27 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md index bf96cf77629..c79fd3c9023 100644 --- a/gamestonk_terminal/portfolio_optimization/README.md +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -1,35 +1,76 @@ # 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 +* [property](#property) + * weight according to selected info property (e.g. marketCap) +* [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 -* [Property Weighting](#weighting) - * Equal Weights - * Market Cap Weighting - * Divident Yield Weighting -* [Mean-Variance-Optimization](#eff_front) - * Max sharpe ratio - * Minimum volatility - * Maximum returns at given risk level - * Minimum risk level at a given return -## Procedure -There are three ways to load stocks to be analyzed. -* add -* select -* from ca menu -###add -Adds selected tickers to the menu to be considered +## 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 : Amount to allocate to portfolio. +* --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 : Amount to allocate to portfolio. +* --pie : Display a pie chart for weights + -```` -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. @@ -166,4 +207,4 @@ And the pie chart: ![yummypie](https://user-images.githubusercontent.com/18151143/114740289-b9167f00-9d17-11eb-9c29-470785b21d09.png) -Note that since `AAPL` had zero allocation, it was omitted from the chart. \ No newline at end of file +Note that since `AAPL` had zero allocation, it was omitted from the chart. From a21195438b4370578b688d7774706e9587801504 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 02:01:17 +0100 Subject: [PATCH 08/37] Update README.md --- .../portfolio_optimization/README.md | 202 +++++++++--------- 1 file changed, 97 insertions(+), 105 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md index c79fd3c9023..2825bf9311b 100644 --- a/gamestonk_terminal/portfolio_optimization/README.md +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -10,6 +10,9 @@ This menu aims to optimize a portfolio of pre-loaded stocks, and the usage of th * equally weighted * [property](#property) * weight according to selected info property (e.g. marketCap) + +[Mean Variance Optimization](#Mean_Variance_Optimization) + * [maxsharpe](#maxsharpe) * optimizes for maximal Sharpe ratio (a.k.a the tangency portfolio) * [minvol](#minvol) @@ -24,7 +27,7 @@ This menu aims to optimize a portfolio of pre-loaded stocks, and the usage of th * show the efficient frontier -## add +### add ```text add [-t ADD_TICKERS] @@ -32,10 +35,10 @@ add [-t ADD_TICKERS] Add/Select tickers for portfolio to be optimized. -* -t : tickers to be used in the portfolio to optimize +* -t : Tickers to be used in the portfolio to optimize * -## select +### select ```text select [-t ADD_TICKERS] @@ -43,10 +46,10 @@ select [-t ADD_TICKERS] Add/Select tickers for portfolio to be optimized. -* -t : tickers to be used in the portfolio to optimize +* -t : Tickers to be used in the portfolio to optimize -## equal +### equal ```text equal [-v VALUE] [--pie] @@ -54,11 +57,11 @@ equal [-v VALUE] [--pie] Returns an equally weighted portfolio -* -v : Amount to allocate to portfolio. -* --pie : Display a pie chart for weights +* -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 +### property ```text property [-p PROPERTY] [-v VALUE] [--pie] @@ -67,122 +70,111 @@ 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 : Amount to allocate to portfolio. -* --pie : Display a pie chart for weights +* -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 - -### ca menu -From the ca menu, the loaded ticker and the selected similar tickers can be loaded by entering the `> po` menu. - -From this `po` menu, you can return to `ca` using `ca`, which will keep your loaded stock, but reset the selected stocks from that menu. - -Once your stocks are listed, you can select one of the options. -## Property weighted -* equal weights -* market cap weighted -* dividend yield weighted -### equal weights -Returns an equally weighted portfolio where the weights are 1/number of stocks. -```` -equal_weight [-v --value VALUE] [--pie] -```` -* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. -* --pie Flag that displays a pie chart of the allocations. - -### market cap weighted -Returns portfolio values that are weighted by their relative Market Cap. -```` -mkt_cap [-v --value VALUE] [--pie] -```` -* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. -* --pie Flag that displays a pie chart of the allocations. - -###dividend yield weighted -Returns portfolio values that are weighted by relative Dividend Yield. -```` -div_yield [-v --value VALUE] [--pie] -```` -* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. -* --pie Flag that displays a pie chart of the allocations. - -## Mean Variance Optimization +## Mean Variance Optimization These approaches are based off of the efficient frontier approach, which is meant to solve the following optimization problem. - +Minimize: -With constraints: +Subject to: , and - +* Where S is the covariance matrix between stocks and R is the expected returns. +* The condition that all weights add up to 1 just implies that you want to have a net long portfolio (with no margin). +* A long-short portfolio can have negative weights and usually wants to have everything add up to 0 for a market-neutral strategy. -\ - +Currently, we do not allow for changing risk models or adding constraints. +If there is something specific, please submit a feature request, or if you can write it, feel free to add a PR! +All of these commands use [PyPortFolioOpt](#https://pyportfolioopt.readthedocs.io/en/latest/index.html) package. -Where S is the covariance matrix between stocks and R is the expected returns. The condition that all weights add up to 1 -just implies that you want to have a net long portfolio (with no margin). -A long-short portfolio can have negative weights and usually wants to have everything add up to 0 for a market-neutral strategy. +### maxsharpe -Currently, we do not allow for changing risk models or adding constraints. If there is something specific, please submit a feature request, or if you can -write it, feel free to add a PR! - -All of our current implementations use the [PyPortFolioOpt](#https://pyportfolioopt.readthedocs.io/en/latest/index.html) package. - -### max_sharpe -The sharpe ratio is defined as - -(Mean Returns - Risk Free Rate)/(Standard Deviation of Returns) - -The usage is: ```` -max_sharpe [-p PERIOD] [-v --value VALUE] [--pie] +usage: max_sharpe [-p {1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max}] [-v VALUE] [--pie] [-r RISK_FREE_RATE] +```` + +Maximise the Sharpe Ratio. The result is also referred to as the tangency portfolio, as it is the portfolio for which the capital market line is tangent to the efficient frontier. This is a convex optimization problem after making a certain variable substitution. See Cornuejols and Tutuncu (2006) for more. The sharpe ratio is defined as (Mean Returns - Risk Free Rate)/(Standard Deviation of Returns). + +* -p : Amount of time to retrieve data from yfinance. Default: 3mo. +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights + + +### 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. -### show_eff +### 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. -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 + + +## 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: ```` From 1878d997869a2209587386707ceb40472d4042c2 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 02:24:53 +0100 Subject: [PATCH 09/37] Add nice plots and stuff --- .../portfolio_optimization/README.md | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md index 2825bf9311b..d3591364c21 100644 --- a/gamestonk_terminal/portfolio_optimization/README.md +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -60,6 +60,8 @@ Returns an equally weighted portfolio * -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. * --pie : Display a pie chart for weights +![equal](https://user-images.githubusercontent.com/25267873/114958292-9771e080-9e5a-11eb-9de6-1c2dfbe1e27b.png) + ### property @@ -73,6 +75,8 @@ Returns a portfolio that is weighted based on a selected property 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 +![marketCap](https://user-images.githubusercontent.com/25267873/114958294-980a7700-9e5a-11eb-9db4-d06e6a1e8981.png) + ## Mean Variance Optimization @@ -90,6 +94,7 @@ Currently, we do not allow for changing risk models or adding constraints. If there is something specific, please submit a feature request, or if you can write it, feel free to add a PR! All of these commands use [PyPortFolioOpt](#https://pyportfolioopt.readthedocs.io/en/latest/index.html) package. + ### maxsharpe ```` @@ -102,6 +107,8 @@ Maximise the Sharpe Ratio. The result is also referred to as the tangency portfo * -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. * --pie : Display a pie chart for weights +maxsharpe + ### minvol @@ -115,6 +122,9 @@ This portfolio minimizes the total volatility, which also means it has the small * -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. * --pie : Display a pie chart for weights +![min_vol](https://user-images.githubusercontent.com/25267873/114958290-96d94a00-9e5a-11eb-9b7c-deba8a8e7249.png) +min_vol2 + ### maxquadutil @@ -130,6 +140,8 @@ Maximises the quadratic utility, given some risk aversion. * -r : Risk aversion parameter * -n : Whether the portfolio should be market neutral (weights sum to zero). Defaults: False. +maxquadutil + ### effret @@ -145,6 +157,8 @@ Calculate the 'Markowitz portfolio', minimising volatility for a given target re * -t : The desired return of the resulting portfolio * -n : Whether the portfolio should be market neutral (weights sum to zero). Defaults: False. +effret + ### effrisk @@ -160,6 +174,9 @@ Maximise return for a target risk. The resulting portfolio will have a volatilit * -t : The desired maximum volatility of the resultingportfolio * -n : Whether the portfolio should be market neutral (weights sum to zero). Defaults: False. +![effrisk](https://user-images.githubusercontent.com/25267873/114958284-95a81d00-9e5a-11eb-8626-988505db8890.png) +effrisk2 + ### ef @@ -172,31 +189,4 @@ This function plots random portfolios based on their risk and returns and shows * -n : Number of portfolios to simulate. Default 300. * -p : Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo. - - -## Sample Usage -In this example, we generate weights for a list of 6 stocks using the eff_ret command. This optimization looks to maximize returns -at a given risk level. We start by adding the stocks we want to analyze: -```` -select aapl,amzn,msft,f,gm,ge -```` -Which shows: -```` -Current Tickers: GE, GM, AMZN, AAPL, F, MSFT -```` -To perform the optimization, we will set a target return of 25% (.25). Given how stocks performed during COVID, -there is a lot of volatility, so many optimizations may not get low volatility. The module also returns annualized volatility, -so the number is your portfolio volatility * `sqrt(252`. This optimization will be (including a pie chart!). Note we could also supply a different -time period, which changes the expected returns and historical volatility, which changes the optimization. We could also specify a dollar -amount that you wish to allocate using the `-v` flag. -```` -eff_ret -r .25 --pie -```` -The console will show (numbers will vary based on when this is done). -![console](https://user-images.githubusercontent.com/18151143/114740311-bd429c80-9d17-11eb-90e2-97430781431a.png) - -And the pie chart: - -![yummypie](https://user-images.githubusercontent.com/18151143/114740289-b9167f00-9d17-11eb-9c29-470785b21d09.png) - -Note that since `AAPL` had zero allocation, it was omitted from the chart. +![ef](https://user-images.githubusercontent.com/25267873/114958281-93de5980-9e5a-11eb-967b-d37ddd4f3e73.png) From 64bdc1da0cd1180784b5062a069b2a6c98ac9ca8 Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 02:26:33 +0100 Subject: [PATCH 10/37] more fixes here and there --- .../portfolio_optimization/optimizer_view.py | 45 +++++++++++++------ .../portfolio_optimization/po_controller.py | 9 ++-- .../technical_analysis/README.md | 2 +- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py index 92ee5417fb9..af081ca4708 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -10,7 +10,11 @@ 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, @@ -130,7 +134,7 @@ def property_weighting(stocks: List[str], other_args: List[str]): parser.add_argument( "-p", "--property", - required=True, + 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: @@ -267,7 +271,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): other_args.insert(0, "-r") parser.add_argument( "-r", - "--risk_free_rate", + "--risk-free-rate", type=float, dest="risk_free_rate", default=0.02, @@ -281,7 +285,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): other_args.insert(0, "-t") parser.add_argument( "-t", - "--target_volatility", + "--target-volatility", type=float, dest="target_volatility", default=0.1, @@ -289,7 +293,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): ) parser.add_argument( "-n", - "--market_neutral", + "--market-neutral", action="store_true", default=False, dest="market_neutral", @@ -303,7 +307,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): other_args.insert(0, "-t") parser.add_argument( "-t", - "--target_return", + "--target-return", type=float, dest="target_return", default=0.1, @@ -311,7 +315,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): ) parser.add_argument( "-n", - "--market_neutral", + "--market-neutral", action="store_true", default=False, dest="market_neutral", @@ -325,7 +329,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): other_args.insert(0, "-r") parser.add_argument( "-r", - "--risk_aversion", + "--risk-aversion", type=float, dest="risk_aversion", default=1, @@ -333,7 +337,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): ) parser.add_argument( "-n", - "--market_neutral", + "--market-neutral", action="store_true", default=False, dest="market_neutral", @@ -357,7 +361,7 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): if port_type == "max_sharpe": ef_opt = dict(ef.max_sharpe(ns_parser.risk_free_rate)) - s_title = f"{sp} Weights that maximize Sharpe ration with risk free level of {ns_parser.risk_free_rate}" + s_title = f"{sp} Weights that maximize Sharpe ratio with risk free level of {ns_parser.risk_free_rate}" elif port_type == "min_volatility": ef_opt = dict(ef.min_volatility()) @@ -407,7 +411,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", @@ -418,15 +427,23 @@ 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 if len(stocks) < 2: - print("Please have at least 2 loaded tickers to calculate weights.") + print("Please have at least 2 loaded tickers to calculate weights.\n") return stock_prices = process_stocks(stocks, ns_parser.period) @@ -449,7 +466,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 24a93bcdd5e..a7ef204f128 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -61,6 +61,7 @@ class PortfolioOptimization: " property weight according to selected info property (e.g. marketCap)" ) print("") + print("Mean Variance Optimization:") print( " maxsharpe optimizes for maximal Sharpe ratio (a.k.a the tangency portfolio)" ) @@ -151,11 +152,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", @@ -163,7 +164,7 @@ 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: 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 ``` From 31959452c7a43d681d9af4264e73d48510c8d08a Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 02:29:29 +0100 Subject: [PATCH 11/37] Update README.md --- gamestonk_terminal/comparison_analysis/README.md | 2 ++ 1 file changed, 2 insertions(+) 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 From 7b94769b793b486f0b81b3eaaa21587467ec5c09 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 02:30:15 +0100 Subject: [PATCH 12/37] Update README.md --- gamestonk_terminal/screener/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 +```` From 5d9ee5c8974fe4a0f10d3114a8cdab9e636c7c71 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 02:42:43 +0100 Subject: [PATCH 13/37] Update ROADMAP.md --- ROADMAP.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index f5c466f3a9a..073316e0213 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) @@ -110,6 +111,7 @@ * [x] Add multi-ticker sentiment comparison (@didier) - [PR #250](https://github.com/DidierRLopes/GamestonkTerminal/pull/250) * [x] Get similar companies from Finviz based on Industry and Sector (and possibly country) from Finviz (@didier) - [PR #323](https://github.com/DidierRLopes/GamestonkTerminal/pull/323) * [x] Output brief screen (overview, performance, financial, valuation) across similar companies (@didier) - [PR #323](https://github.com/DidierRLopes/GamestonkTerminal/pull/323) +* [x] Add command to take tickers to portfolio optimization (@jmaslek) - [PR #329](https://github.com/DidierRLopes/GamestonkTerminal/pull/329) **NEXT** @@ -142,13 +144,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 @@ -189,6 +195,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** From 886e1830c0e4a163677cc4df34521f46d90f30a7 Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 02:58:12 +0100 Subject: [PATCH 14/37] im a picky guy, sorry --- gamestonk_terminal/comparison_analysis/ca_controller.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py index e0a339202c2..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 From 1ceb93861051a42cc4038963ed234716d05e0b56 Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Fri, 16 Apr 2021 10:02:31 +0200 Subject: [PATCH 15/37] change handling and listing of the latest news from SeekingAlpha --- .../discovery/disc_controller.py | 10 ++--- .../discovery/seeking_alpha_model.py | 18 ++++----- .../discovery/seeking_alpha_view.py | 37 ++++++++++++------- 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index fa0f893232f..9feec08025a 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -46,7 +46,7 @@ class DiscoveryController: "valuation", "performance", "spectrum", - "market_news", + "latest", "news_article" ] @@ -89,7 +89,7 @@ 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(" market_news latest news [SeekingAlpha]") + print(" latest latest news [SeekingAlpha]") print(" news_article news article [SeekingAlpha]") print("") @@ -191,9 +191,9 @@ class DiscoveryController: other_args, "spectrum" ) - def call_market_news(self, other_args: List[str]): - """Process market_news command""" - seeking_alpha_view.articles_list_view(other_args) + def call_latest(self, other_args: List[str]): + """Process latest command""" + seeking_alpha_view.latest_news_view(other_args) def call_news_article(self, other_args: List[str]): """Process news_article command""" diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index 06ec76e8b15..aa74045c041 100755 --- a/gamestonk_terminal/discovery/seeking_alpha_model.py +++ b/gamestonk_terminal/discovery/seeking_alpha_model.py @@ -89,13 +89,13 @@ def get_articles_html(url_articles: str) -> str: return articles_html -def get_article_list(pages: int) -> list: - """Returns a list of recent articles +def get_article_list(num: int) -> list: + """Returns a list of latest articles Parameters ---------- pages : int - Number of pages + Number of articles Returns ------- @@ -104,8 +104,9 @@ def get_article_list(pages: int) -> list: """ articles = list() - url_articles = "https://seekingalpha.com/market-news" - for idx in range(0, pages): + 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", @@ -121,14 +122,13 @@ def get_article_list(pages: int) -> list: article_id = article_url.split('/')[2].split('-')[0] articles.append({ 'title': item.text, - 'date': item_row['data-last-date'], + 'publishedAt': item_row['data-last-date'], 'url': "https://seekingalpha.com"+article_url, 'id': article_id }) - url_articles = ( - f"https://seekingalpha.com/market-news/{idx+1}" - ) + page += 1 + url_articles = f"https://seekingalpha.com/market-news/{page}" return articles diff --git a/gamestonk_terminal/discovery/seeking_alpha_view.py b/gamestonk_terminal/discovery/seeking_alpha_view.py index f54714b11cc..eff8c341112 100644 --- a/gamestonk_terminal/discovery/seeking_alpha_view.py +++ b/gamestonk_terminal/discovery/seeking_alpha_view.py @@ -67,38 +67,49 @@ def earnings_release_dates_view(other_args: List[str]): print("") -def articles_list_view(other_args: List[str]): - """Prints a news article list +def latest_news_view(other_args: List[str]): + """Prints the latest news article list Parameters ---------- other_args : List[str] - argparse other args - ["-p", "20", "-n", "5"] + argparse other args - ["-n", "5"] """ parser = argparse.ArgumentParser( add_help=False, - prog="up_articles", - description="""Latest articles. [Source: Seeking Alpha]""", + prog="latest", + description="""Latest news articles. [Source: Seeking Alpha]""", ) parser.add_argument( - "-p", - "--pages", + "-n", + "--num", action="store", - dest="n_pages", + dest="n_num", type=check_positive, - default=1, - help="Number of pages from Seeking Alpha website.", + default=10, + help="number of latest articles being printed.", ) ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return - articles = seeking_alpha_model.get_article_list(ns_parser.n_pages) - for article in articles: - print(article['id'], '-', article['title'], '(', article['url'], ')') + 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 def news_article_view(other_args: List[str]): From f2bc9c1cbe689faf42b7c62be691e8a78bda1194 Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Fri, 16 Apr 2021 10:44:48 +0200 Subject: [PATCH 16/37] add trending news list, from Seeking Alpha --- .../discovery/disc_controller.py | 6 +++ .../discovery/seeking_alpha_model.py | 37 +++++++++++++++ .../discovery/seeking_alpha_view.py | 45 +++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index 9feec08025a..12bc5149ad3 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -47,6 +47,7 @@ class DiscoveryController: "performance", "spectrum", "latest", + "trending", "news_article" ] @@ -90,6 +91,7 @@ class DiscoveryController: print(" performance performance of sectors, industry, country [Finviz]") print(" spectrum spectrum of sectors, industry, country [Finviz]") print(" latest latest news [SeekingAlpha]") + print(" trending trending news [SeekingAlpha]") print(" news_article news article [SeekingAlpha]") print("") @@ -195,6 +197,10 @@ class DiscoveryController: """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_news_article(self, other_args: List[str]): """Process news_article command""" seeking_alpha_view.news_article_view(other_args) diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index aa74045c041..684e009390a 100755 --- a/gamestonk_terminal/discovery/seeking_alpha_model.py +++ b/gamestonk_terminal/discovery/seeking_alpha_model.py @@ -133,6 +133,43 @@ def get_article_list(num: int) -> list: 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'], + 'url': "https://seekingalpha.com"+article_url, + 'id': article_id + }) + + return articles + + def get_article_data(article_id: int) -> dict: """Returns an article diff --git a/gamestonk_terminal/discovery/seeking_alpha_view.py b/gamestonk_terminal/discovery/seeking_alpha_view.py index eff8c341112..0d3764c1a34 100644 --- a/gamestonk_terminal/discovery/seeking_alpha_view.py +++ b/gamestonk_terminal/discovery/seeking_alpha_view.py @@ -112,6 +112,51 @@ def latest_news_view(other_args: List[str]): break +def trending_news_view(other_args: List[str]): + """Prints the trending news article list + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-n", "5"] + """ + + parser = argparse.ArgumentParser( + add_help=False, + prog="trending", + description="""Trending news articles. [Source: Seeking Alpha]""", + ) + + parser.add_argument( + "-n", + "--num", + action="store", + dest="n_num", + type=check_positive, + default=10, + help="number of trending articles being printed.", + ) + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + 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 + + def news_article_view(other_args: List[str]): """Prints a news article From 7d3e9de61f4a4cffe2fc4ac52e6263b8398aa8f3 Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Fri, 16 Apr 2021 11:00:50 +0200 Subject: [PATCH 17/37] small fixes to the article print output --- gamestonk_terminal/discovery/seeking_alpha_model.py | 3 ++- gamestonk_terminal/discovery/seeking_alpha_view.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index 684e009390a..5db84d9bb11 100755 --- a/gamestonk_terminal/discovery/seeking_alpha_model.py +++ b/gamestonk_terminal/discovery/seeking_alpha_model.py @@ -192,7 +192,8 @@ def get_article_data(article_id: int) -> dict: article = { 'title': jdata['data']['attributes']['title'], - 'date': jdata['data']['attributes']['lastModified'], + 'publishedAt': jdata['data']['attributes']['lastModified'], + 'url': "https://seekingalpha.com"+jdata['data']['links']['self'], 'content': content } diff --git a/gamestonk_terminal/discovery/seeking_alpha_view.py b/gamestonk_terminal/discovery/seeking_alpha_view.py index 0d3764c1a34..f717426f66c 100644 --- a/gamestonk_terminal/discovery/seeking_alpha_view.py +++ b/gamestonk_terminal/discovery/seeking_alpha_view.py @@ -189,5 +189,7 @@ def news_article_view(other_args: List[str]): return article = seeking_alpha_model.get_article_data(ns_parser.n_id) - print(article['date'], article['title']) + print(article['publishedAt'], " ", article['title']) + print(article["url"]) + print("") print(article['content']) From ac08309abcc617bfb368adeaa1128f6280a996fd Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Fri, 16 Apr 2021 11:34:12 +0200 Subject: [PATCH 18/37] comply with cosmetics requirements --- .../discovery/disc_controller.py | 2 +- .../discovery/seeking_alpha_model.py | 55 ++++++++++--------- .../discovery/seeking_alpha_view.py | 14 ++--- 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index 12bc5149ad3..a9a66116107 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -48,7 +48,7 @@ class DiscoveryController: "spectrum", "latest", "trending", - "news_article" + "news_article", ] def __init__(self): diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index 5db84d9bb11..7e85ca8ea70 100755 --- a/gamestonk_terminal/discovery/seeking_alpha_model.py +++ b/gamestonk_terminal/discovery/seeking_alpha_model.py @@ -1,6 +1,7 @@ """ Seeking Alpha Model """ __docformat__ = "numpy" +from typing import List import requests from bs4 import BeautifulSoup import pandas as pd @@ -89,7 +90,7 @@ def get_articles_html(url_articles: str) -> str: return articles_html -def get_article_list(num: int) -> list: +def get_article_list(num: int) -> List[dict]: """Returns a list of latest articles Parameters @@ -103,7 +104,7 @@ def get_article_list(num: int) -> list: Latest articles list """ - articles = list() + articles: List[dict] = [] page = 1 url_articles = f"https://seekingalpha.com/market-news/{page}" while len(articles) < num: @@ -116,16 +117,18 @@ def get_article_list(num: int) -> list: item = item_row.find("a", {"class": "add-source-assigned"}) if item is None: continue - article_url = item['href'] - if not article_url.startswith('/news/'): + 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': item_row['data-last-date'], - 'url': "https://seekingalpha.com"+article_url, - 'id': article_id - }) + article_id = article_url.split("/")[2].split("-")[0] + articles.append( + { + "title": item.text, + "publishedAt": item_row["data-last-date"], + "url": "https://seekingalpha.com" + article_url, + "id": article_id, + } + ) page += 1 url_articles = f"https://seekingalpha.com/market-news/{page}" @@ -156,16 +159,18 @@ def get_trending_list(num: int) -> list: print("Invalid response\n") else: for item in response.json(): - article_url = item['uri'] - if not article_url.startswith('/news/'): + 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'], - 'url': "https://seekingalpha.com"+article_url, - 'id': article_id - }) + article_id = article_url.split("/")[2].split("-")[0] + articles.append( + { + "title": item["title"], + "publishedAt": item["publish_on"], + "url": "https://seekingalpha.com" + article_url, + "id": article_id, + } + ) return articles @@ -187,14 +192,14 @@ def get_article_data(article_id: int) -> dict: 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 = 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 + "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 f717426f66c..639e145f0d0 100644 --- a/gamestonk_terminal/discovery/seeking_alpha_view.py +++ b/gamestonk_terminal/discovery/seeking_alpha_view.py @@ -89,7 +89,7 @@ def latest_news_view(other_args: List[str]): dest="n_num", type=check_positive, default=10, - help="number of latest articles being printed.", + help="number of latest articles being printed", ) ns_parser = parse_known_args_and_warn(parser, other_args) @@ -101,7 +101,7 @@ def latest_news_view(other_args: List[str]): print( article["publishedAt"].replace("T", " ").replace("Z", ""), "-", - article['id'], + article["id"], "-", article["title"], ) @@ -134,7 +134,7 @@ def trending_news_view(other_args: List[str]): dest="n_num", type=check_positive, default=10, - help="number of trending articles being printed.", + help="number of trending articles being printed", ) ns_parser = parse_known_args_and_warn(parser, other_args) @@ -146,7 +146,7 @@ def trending_news_view(other_args: List[str]): print( article["publishedAt"].replace("T", " ").replace("Z", ""), "-", - article['id'], + article["id"], "-", article["title"], ) @@ -179,7 +179,7 @@ def news_article_view(other_args: List[str]): dest="n_id", type=check_positive, default=-1, - help="Article number found on Seeking Alpha website.", + help="article number found on Seeking Alpha website", ) ns_parser = parse_known_args_and_warn(parser, other_args) @@ -189,7 +189,7 @@ def news_article_view(other_args: List[str]): return article = seeking_alpha_model.get_article_data(ns_parser.n_id) - print(article['publishedAt'], " ", article['title']) + print(article["publishedAt"], " ", article["title"]) print(article["url"]) print("") - print(article['content']) + print(article["content"]) From 58dc9ee52dce121c1973892a6da11053af9f14ab Mon Sep 17 00:00:00 2001 From: Jamie Date: Fri, 16 Apr 2021 14:43:40 -0400 Subject: [PATCH 19/37] add in option chains :) --- gamestonk_terminal/config_terminal.py | 3 + gamestonk_terminal/options/README.md | 29 ++- gamestonk_terminal/options/chains_view.py | 208 ++++++++++++++++++++ gamestonk_terminal/options/op_controller.py | 9 +- gamestonk_terminal/options/volume_view.py | 2 +- 5 files changed, 246 insertions(+), 5 deletions(-) create mode 100644 gamestonk_terminal/options/chains_view.py 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/options/README.md b/gamestonk_terminal/options/README.md index a86dceb2b2c..980be7495c4 100644 --- a/gamestonk_terminal/options/README.md +++ b/gamestonk_terminal/options/README.md @@ -1,11 +1,13 @@ # OPTIONS DATA -This menu aims to give insight on options. options can cause significant share price movement, and option pricing gives info about market sentiment for a ticker. +This menu aims to give insight on options. options can cause significant share price movement, and option pricing gives +info about market sentiment for a ticker. * [volume](#volume) * Show traded options for a given expiry [Yahoo Finance] - +* [chains](#chains) + * Display option chains for ## volume @@ -23,3 +25,26 @@ usage: volume --expiry yyyy-mm-dd Display volume graph for a date. [Source: Yahoo Finance] * -e, --expiry : expiration date. + +## chains +Display a table of options chains. + +This data requires a Tradier sandbox developer account, which can be made [here](#https://tradier.com/products/market-data-api) + +```` +usage: chains [--calls] [--puts] [-m MIN] [-M MAX] +```` +* --calls : Flag to only display call options +* --puts : Flag to display only put options +* -m : Minimum strike price to show +* -M : Maximum strike price to show + +For combines tables, call options are on the left and put options are on the right of the strike + +As an example, if I load my ticker and select an expiry, to view the options chain for strikes between $129 and $135, one would use: + +```` +chains -m 129 -M 135 +```` +Which outputs: +![im](https://user-images.githubusercontent.com/18151143/115068041-b191da00-9ebf-11eb-941e-4e255787ab94.png) diff --git a/gamestonk_terminal/options/chains_view.py b/gamestonk_terminal/options/chains_view.py new file mode 100644 index 00000000000..c05552a909e --- /dev/null +++ b/gamestonk_terminal/options/chains_view.py @@ -0,0 +1,208 @@ +"""Helper functions for the options menu""" +__docformat__ = "numpy" + +import argparse +from typing import List +import requests +import pandas as pd +import numpy as np +from tabulate import tabulate +from gamestonk_terminal.config_terminal import TRADIER_TOKEN +from gamestonk_terminal.helper_funcs import ( + check_non_negative, + parse_known_args_and_warn, +) + +option_columns = [ + "bid", + "ask", + "strike", + "bidsize", + "asksize", + "volume", + "open_interest", + "option_type", +] +greek_columns = ["delta", "gamma", "theta", "vega", "ask_iv", "bid_iv", "mid_iv"] +df_columns = option_columns + greek_columns +column_map = {"mid_iv": "iv", "open_interest": "oi", "volume": "vol"} + + +def process_chains(response: requests.models.Response) -> pd.DataFrame: + """ + Function to take in the requests.get and return a DataFrame + Parameters + ---------- + response: requests.models.Response + This is the respnose from tradier api. + + Returns + ------- + opt_chain: pd.DataFrame + Dataframe with all available options + """ + json_response = response.json() + options = json_response["options"]["option"] + + opt_chain = pd.DataFrame(columns=df_columns) + for idx, option in enumerate(options): + data = [option[col] for col in option_columns] + data += [option["greeks"][col] for col in greek_columns] + opt_chain.loc[idx, :] = data + + return opt_chain + + +def get_option_chains(symbol: str, expiry: str) -> pd.DataFrame: + """ + + Parameters + ---------- + symbol : str + Ticker to get options for + expiry : str + Expiration date in the form of "YYYY-MM-DD" + + Returns + ------- + chains: pd.DataFrame + Dataframe with options for the gievn Symbol and Expiration date + """ + params = {"symbol": symbol, "expiration": expiry, "greeks": "true"} + + headers = {"Authorization": f"Bearer {TRADIER_TOKEN}", "Accept": "application/json"} + + response = requests.get( + "https://sandbox.tradier.com/v1/markets/options/chains", + params=params, + headers=headers, + ) + if response.status_code == 401: + print("Error in request.get -- check token") + return + + chains = process_chains(response) + return chains + + +def display_chains(symbol: str, expiry: str, other_args: List[str]): + + parser = argparse.ArgumentParser( + prog="chains", add_help=False, description="Display options chains" + ) + + parser.add_argument( + "--calls", + action="store_true", + default=False, + dest="calls_only", + help="Flag to show calls only", + ) + + parser.add_argument( + "--puts", + action="store_true", + default=False, + dest="puts_only", + help="Flag to show puts only", + ) + parser.add_argument( + "-m", + "--min", + dest="min_sp", + type=check_non_negative, + default=-1, + help="minimum strike price to consider.", + ) + parser.add_argument( + "-M", + "--max", + dest="max_sp", + type=check_non_negative, + default=-1, + help="maximum strike price to consider.", + ) + + chains_df = get_option_chains(symbol, expiry) + + default_columns = [ + "mid_iv", + "delta", + "volume", + "open_interest", + "bid", + "ask", + "strike", + "option_type", + ] + chains_df = chains_df[default_columns].rename(columns=column_map) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + if ns_parser.max_sp == -1 and ns_parser.min_sp == -1: + min_strike = np.percentile(chains_df["strike"], 25) + max_strike = np.percentile(chains_df["strike"], 75) + else: + min_strike = ns_parser.min_sp + max_strike = ns_parser.max_sp + + chains_df = chains_df[chains_df["strike"] >= min_strike] + chains_df = chains_df[chains_df["strike"] <= max_strike] + calls_df = chains_df[chains_df.option_type == "call"].drop( + columns=["option_type"] + ) + puts_df = chains_df[chains_df.option_type == "put"].drop( + columns=["option_type"] + ) + + if ns_parser.calls_only: + print( + tabulate( + calls_df, + headers=calls_df.columns, + tablefmt="grid", + showindex=False, + floatfmt=".2f", + ) + ) + return + + if ns_parser.puts_only: + print( + tabulate( + puts_df, + headers=puts_df.columns, + tablefmt="grid", + showindex=False, + floatfmt=".2f", + ) + ) + return + + puts_df = puts_df[puts_df.columns[::-1]] + chain_table = calls_df.merge(puts_df, on="strike") + + headers = [ + col.strip("_x") + if col.endswith("_x") + else col.strip("_y") + if col.endswith("_y") + else col + for col in chain_table.columns + ] + print( + tabulate( + chain_table, + headers=headers, + tablefmt="fancy_grid", + showindex=False, + floatfmt=".2f", + ) + ) + + except Exception as e: + print(e) + print("") + return diff --git a/gamestonk_terminal/options/op_controller.py b/gamestonk_terminal/options/op_controller.py index e373ba1f677..4caa51dbd63 100644 --- a/gamestonk_terminal/options/op_controller.py +++ b/gamestonk_terminal/options/op_controller.py @@ -9,7 +9,7 @@ import yfinance as yf from prompt_toolkit.completion import NestedCompleter from gamestonk_terminal.helper_funcs import get_flair, parse_known_args_and_warn from gamestonk_terminal import feature_flags as gtff -from gamestonk_terminal.options import volume_view +from gamestonk_terminal.options import volume_view, chains_view from gamestonk_terminal.menu import session @@ -17,7 +17,7 @@ class OptionsController: """Options Controller class.""" # Command choices - CHOICES = ["help", "q", "quit", "exp", "voi", "vcalls", "vputs"] + CHOICES = ["help", "q", "quit", "exp", "voi", "vcalls", "vputs", "chains"] def __init__(self, ticker: str, last_adj_close_price: float): """Construct data.""" @@ -93,6 +93,8 @@ class OptionsController: print(" vcalls calls volume + open interest plot") print(" vputs puts volume + open interest plot") print("") + print(" chains display option chains") + print("") return def switch(self, an_input: str): @@ -158,6 +160,9 @@ class OptionsController: self.options.puts, ) + def call_chains(self, other_args): + chains_view.display_chains(self.ticker, self.expiry_date, other_args) + def menu(ticker: str, last_adj_close_price: float): """ Options Menu. """ diff --git a/gamestonk_terminal/options/volume_view.py b/gamestonk_terminal/options/volume_view.py index a20e4cafc54..a4f3062a960 100644 --- a/gamestonk_terminal/options/volume_view.py +++ b/gamestonk_terminal/options/volume_view.py @@ -2,12 +2,12 @@ __docformat__ = "numpy" from bisect import bisect_left -import seaborn as sns from typing import List import argparse import pandas as pd import matplotlib.pyplot as plt import numpy as np +import seaborn as sns from gamestonk_terminal.helper_funcs import ( plot_autoscale, From 245f3ddc6df7f121e0ec48b9a4423dfd0baec63b Mon Sep 17 00:00:00 2001 From: Jamie Date: Fri, 16 Apr 2021 14:47:31 -0400 Subject: [PATCH 20/37] update main readme with API key --- README.md | 6 +++++- gamestonk_terminal/feature_flags.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) 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/gamestonk_terminal/feature_flags.py b/gamestonk_terminal/feature_flags.py index 5c4910a1ab2..bd9c048cacc 100644 --- a/gamestonk_terminal/feature_flags.py +++ b/gamestonk_terminal/feature_flags.py @@ -11,7 +11,7 @@ USE_FLAIR = os.getenv("GTFF_USE_FLAIR") or "stars" USE_ION = strtobool(os.getenv("GTFF_USE_ION", "True")) # Enable Prompt Toolkit -USE_PROMPT_TOOLKIT = strtobool(os.getenv("GTFF_USE_PROMPT_TOOLKIT", "False")) +USE_PROMPT_TOOLKIT = True # strtobool(os.getenv("GTFF_USE_PROMPT_TOOLKIT", "False")) # Enable Prediction features ENABLE_PREDICT = strtobool(os.getenv("GTFF_ENABLE_PREDICT", "False")) From 3afd747fd45f202a3fe08e3c5365494df864ee19 Mon Sep 17 00:00:00 2001 From: Jamie Date: Fri, 16 Apr 2021 14:48:08 -0400 Subject: [PATCH 21/37] update main readme with API key --- gamestonk_terminal/feature_flags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gamestonk_terminal/feature_flags.py b/gamestonk_terminal/feature_flags.py index bd9c048cacc..5c4910a1ab2 100644 --- a/gamestonk_terminal/feature_flags.py +++ b/gamestonk_terminal/feature_flags.py @@ -11,7 +11,7 @@ USE_FLAIR = os.getenv("GTFF_USE_FLAIR") or "stars" USE_ION = strtobool(os.getenv("GTFF_USE_ION", "True")) # Enable Prompt Toolkit -USE_PROMPT_TOOLKIT = True # strtobool(os.getenv("GTFF_USE_PROMPT_TOOLKIT", "False")) +USE_PROMPT_TOOLKIT = strtobool(os.getenv("GTFF_USE_PROMPT_TOOLKIT", "False")) # Enable Prediction features ENABLE_PREDICT = strtobool(os.getenv("GTFF_ENABLE_PREDICT", "False")) From 72a2337bbbce32ecd34ec973686835b0a8ea09e6 Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 21:51:46 +0100 Subject: [PATCH 22/37] Refactor commands because OCD --- .../portfolio_optimization/optimizer_view.py | 556 ++++++++++++++---- .../portfolio_optimization/po_controller.py | 16 +- 2 files changed, 439 insertions(+), 133 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py index af081ca4708..f54ccdc3ea3 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -94,9 +94,11 @@ 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 = {} for stock in stocks: values[stock] = ns_parser.value * round(1 / len(stocks), 5) @@ -208,38 +210,22 @@ def property_weighting(stocks: List[str], other_args: List[str]): 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, eff_risk, eff_ret) Returns ------- weights: dict Dictionary of weights where keys are the tickers. """ - - d_descriptions = { - "max_sharpe": """Maximise the Sharpe Ratio. The result is also referred to as the tangency portfolio, - as it is the portfolio for which the capital market line is tangent to the efficient frontier. - - This is a convex optimization problem after making a certain variable substitution. See - Cornuejols and Tutuncu (2006) for more.""", - "min_volatility": "optimizes for minimum volatility", - "max_quadratic_utility": "maximises the quadratic utility, given some risk aversion", - "efficient_risk": """Maximise return for a target risk. The resulting portfolio will have - a volatility less than the target (but not guaranteed to be equal).""", - "efficient_return": "Calculate the 'Markowitz portfolio', minimising volatility for a given target return.", - } - parser = argparse.ArgumentParser( - add_help=False, prog=port_type, description=d_descriptions[port_type] + add_help=False, prog="maxsharpe", description="Maximise the Sharpe Ratio" ) parser.add_argument( "-p", @@ -265,90 +251,24 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): help="Display a pie chart for weights", ) - if port_type == "max_sharpe": - 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.""", - ) - - elif port_type == "efficient_risk": - 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", - ) - 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.""", - ) - - elif port_type == "efficient_return": - 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", - ) - 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.""", - ) - - elif port_type == "max_quadratic_utility": - 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", - ) - 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 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 @@ -359,36 +279,8 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): sp = d_period[ns_parser.period] - if port_type == "max_sharpe": - 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}" - - elif port_type == "min_volatility": - ef_opt = dict(ef.min_volatility()) - s_title = f"{sp} Weights that minimize volatility" - - elif port_type == "efficient_risk": - 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}" - - elif port_type == "efficient_return": - 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}" - - elif port_type == "max_quadratic_utility": - 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}" - - else: - raise ValueError("EF Method not found") + 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() @@ -410,6 +302,414 @@ def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): 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, diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index a7ef204f128..398cbc883ab 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -5,11 +5,13 @@ __docformat__ = "numpy" import argparse from typing import List import matplotlib.pyplot as plt +from datetime import datetime 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 +from gamestonk_terminal.comparison_analysis import ca_controller class PortfolioOptimization: @@ -28,6 +30,7 @@ class PortfolioOptimization: "effrisk", "maxquadutil", "ef", + "ca", "yolo", ] @@ -74,6 +77,9 @@ class PortfolioOptimization: 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 @@ -122,23 +128,23 @@ class PortfolioOptimization: def call_maxsharpe(self, other_args: List[str]): """Process maxsharpe command""" - optimizer_view.ef_portfolio(self.tickers, "max_sharpe", other_args) + optimizer_view.max_sharpe(self.tickers, other_args) def call_minvol(self, other_args: List[str]): """Process minvol command""" - optimizer_view.ef_portfolio(self.tickers, "min_volatility", other_args) + optimizer_view.min_volatility(self.tickers, other_args) def call_maxquadutil(self, other_args: List[str]): """Process maxquadutil command""" - optimizer_view.ef_portfolio(self.tickers, "max_quadratic_utility", other_args) + optimizer_view.max_quadratic_utility(self.tickers, other_args) def call_effrisk(self, other_args: List[str]): """Process effrisk command""" - optimizer_view.ef_portfolio(self.tickers, "efficient_risk", other_args) + optimizer_view.efficient_risk(self.tickers, other_args) def call_effret(self, other_args: List[str]): """Process effret command""" - optimizer_view.ef_portfolio(self.tickers, "efficient_return", other_args) + optimizer_view.efficient_return(self.tickers, other_args) def call_ef(self, other_args): """Process ef command""" From 26923bfa07bf7245542af9948f6dd3a86b1c04e2 Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 22:01:19 +0100 Subject: [PATCH 23/37] Add dividend and mktcap back as commands. Fix not finding property different than None --- .../portfolio_optimization/optimizer_view.py | 6 ++++++ .../portfolio_optimization/po_controller.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py index f54ccdc3ea3..6c61666d5bf 100644 --- a/gamestonk_terminal/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -193,6 +193,12 @@ def property_weighting(stocks: List[str], other_args: List[str]): 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 diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index 398cbc883ab..98fb0cf4557 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -23,6 +23,8 @@ class PortfolioOptimization: "select", "add", "equal", + "mktcap", + "dividend", "property", "maxsharpe", "minvol", @@ -60,6 +62,10 @@ class PortfolioOptimization: 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 (e.g. marketCap)" ) @@ -122,6 +128,16 @@ class PortfolioOptimization: """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) From c8a5fa81deed66f0eef5414ed85510a591650af5 Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 22:03:38 +0100 Subject: [PATCH 24/37] Refactor po_controller helper --- gamestonk_terminal/portfolio_optimization/po_controller.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index 98fb0cf4557..0913f2b50d4 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -66,9 +66,7 @@ class PortfolioOptimization: print( " dividend weighted according to dividend yield (property dividendYield)" ) - print( - " property weight according to selected info property (e.g. marketCap)" - ) + print(" property weight according to selected info property") print("") print("Mean Variance Optimization:") print( From 6b85a2bcd38477d8fdccf1573b4ee3a857755a66 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 22:12:18 +0100 Subject: [PATCH 25/37] Update README.md --- .../portfolio_optimization/README.md | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md index d3591364c21..6542f2476c5 100644 --- a/gamestonk_terminal/portfolio_optimization/README.md +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -8,8 +8,12 @@ This menu aims to optimize a portfolio of pre-loaded stocks, and the usage of th * 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 (e.g. marketCap) + * weight according to selected info property [Mean Variance Optimization](#Mean_Variance_Optimization) @@ -63,6 +67,34 @@ Returns an equally weighted portfolio ![equal](https://user-images.githubusercontent.com/25267873/114958292-9771e080-9e5a-11eb-9de6-1c2dfbe1e27b.png) +### mktcap + +```text +property -p marketCap [-v VALUE] [--pie] +``` + +Returns a portfolio that is weighted based on market cap info + +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights + +![mktCap](https://user-images.githubusercontent.com/25267873/115084151-57fdcf80-9f00-11eb-885c-bfe93de0eb6b.png) + + +### dividend + +```text +property -p dividend [-v VALUE] [--pie] +``` + +Returns a portfolio that is weighted based on dividend yield info + +* -v : If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie : Display a pie chart for weights + +Captura de ecrã 2021-04-16, às 22 11 41 + + ### property ```text From f97a2eea1d9de103db4a7790c732d0ac4b437f5a Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 22:19:23 +0100 Subject: [PATCH 26/37] Update README.md --- gamestonk_terminal/portfolio_optimization/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md index 6542f2476c5..3e9e44d4603 100644 --- a/gamestonk_terminal/portfolio_optimization/README.md +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -114,9 +114,9 @@ Returns a portfolio that is weighted based on a selected property info These approaches are based off of the efficient frontier approach, which is meant to solve the following optimization problem. -Minimize: - -Subject to: , and +

+ +

* Where S is the covariance matrix between stocks and R is the expected returns. * The condition that all weights add up to 1 just implies that you want to have a net long portfolio (with no margin). From 17416405332b6eef9b4328833b27cba9ede6ea9b Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 22:42:36 +0100 Subject: [PATCH 27/37] Improve datetime shown in trending/latest disc news --- gamestonk_terminal/discovery/seeking_alpha_model.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index 7e85ca8ea70..a4857827a0a 100755 --- a/gamestonk_terminal/discovery/seeking_alpha_model.py +++ b/gamestonk_terminal/discovery/seeking_alpha_model.py @@ -6,6 +6,7 @@ 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 @@ -121,10 +122,15 @@ def get_article_list(num: int) -> List[dict]: if not article_url.startswith("/news/"): continue article_id = article_url.split("/")[2].split("-")[0] + articles.append( { "title": item.text, - "publishedAt": item_row["data-last-date"], + "publishedAt": datetime.strptime( + item_row["data-last-date"], "%Y-%m-%d %H:%M:%S %z" + ) + .time() + .strftime("%Y-%m-%d %H:%M:%S"), "url": "https://seekingalpha.com" + article_url, "id": article_id, } @@ -166,7 +172,7 @@ def get_trending_list(num: int) -> list: articles.append( { "title": item["title"], - "publishedAt": item["publish_on"], + "publishedAt": item["publish_on"][: item["publish_on"].rfind(".")], "url": "https://seekingalpha.com" + article_url, "id": article_id, } From 1bba88b2e671ebd21bd74fd5a080e3b9879cc18d Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 23:10:50 +0100 Subject: [PATCH 28/37] refactor to allow to access articles from latest/trending commands --- .../discovery/disc_controller.py | 6 - .../discovery/seeking_alpha_view.py | 140 ++++++++++-------- 2 files changed, 78 insertions(+), 68 deletions(-) diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index a9a66116107..d74eaf19814 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -48,7 +48,6 @@ class DiscoveryController: "spectrum", "latest", "trending", - "news_article", ] def __init__(self): @@ -92,7 +91,6 @@ class DiscoveryController: print(" spectrum spectrum of sectors, industry, country [Finviz]") print(" latest latest news [SeekingAlpha]") print(" trending trending news [SeekingAlpha]") - print(" news_article news article [SeekingAlpha]") print("") def switch(self, an_input: str): @@ -201,10 +199,6 @@ class DiscoveryController: """Process trending command""" seeking_alpha_view.trending_news_view(other_args) - def call_news_article(self, other_args: List[str]): - """Process news_article command""" - seeking_alpha_view.news_article_view(other_args) - def menu(): """Discovery Menu""" diff --git a/gamestonk_terminal/discovery/seeking_alpha_view.py b/gamestonk_terminal/discovery/seeking_alpha_view.py index 639e145f0d0..a11c548be72 100644 --- a/gamestonk_terminal/discovery/seeking_alpha_view.py +++ b/gamestonk_terminal/discovery/seeking_alpha_view.py @@ -73,15 +73,22 @@ def latest_news_view(other_args: List[str]): Parameters ---------- other_args : List[str] - argparse other args - ["-n", "5"] + 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", @@ -92,24 +99,44 @@ def latest_news_view(other_args: List[str]): 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 - articles = seeking_alpha_model.get_article_list(ns_parser.n_num) - for idx, article in enumerate(articles): + # 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"].replace("T", " ").replace("Z", ""), - "-", - article["id"], - "-", + article["publishedAt"][: article["publishedAt"].rfind(":") - 3].replace( + "T", " " + ), + " ", article["title"], ) print(article["url"]) print("") - - if idx >= ns_parser.n_num - 1: - break + print(article["content"]) def trending_news_view(other_args: List[str]): @@ -118,15 +145,22 @@ def trending_news_view(other_args: List[str]): Parameters ---------- other_args : List[str] - argparse other args - ["-n", "5"] + 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", @@ -137,59 +171,41 @@ def trending_news_view(other_args: List[str]): 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 - articles = seeking_alpha_model.get_trending_list(ns_parser.n_num) - for idx, article in enumerate(articles): + # 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"].replace("T", " ").replace("Z", ""), - "-", - article["id"], - "-", + article["publishedAt"][: article["publishedAt"].rfind(":") - 3].replace( + "T", " " + ), + " ", article["title"], ) print(article["url"]) print("") - - if idx >= ns_parser.n_num - 1: - break - - -def news_article_view(other_args: List[str]): - """Prints a news article - - Parameters - ---------- - other_args : List[str] - argparse other args - ["-i", "3681944"] - """ - - parser = argparse.ArgumentParser( - add_help=False, - prog="up_article", - description="""News article. [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", - ) - - ns_parser = parse_known_args_and_warn(parser, other_args) - if not ns_parser: - return - if ns_parser.n_id == -1: - return - - article = seeking_alpha_model.get_article_data(ns_parser.n_id) - print(article["publishedAt"], " ", article["title"]) - print(article["url"]) - print("") - print(article["content"]) + print(article["content"]) From d3b17532f0be873e013d2c8f5cd9915512263e1c Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 23:14:57 +0100 Subject: [PATCH 29/37] minor typo --- gamestonk_terminal/discovery/seeking_alpha_model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index a4857827a0a..3f056b22a2e 100755 --- a/gamestonk_terminal/discovery/seeking_alpha_model.py +++ b/gamestonk_terminal/discovery/seeking_alpha_model.py @@ -129,7 +129,6 @@ def get_article_list(num: int) -> List[dict]: "publishedAt": datetime.strptime( item_row["data-last-date"], "%Y-%m-%d %H:%M:%S %z" ) - .time() .strftime("%Y-%m-%d %H:%M:%S"), "url": "https://seekingalpha.com" + article_url, "id": article_id, From 1c8994abe9a48fc08ef2513ecb8fd2c979235e69 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 23:22:56 +0100 Subject: [PATCH 30/37] Update README.md --- gamestonk_terminal/discovery/README.md | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/gamestonk_terminal/discovery/README.md b/gamestonk_terminal/discovery/README.md index 517d142d334..2e69e69f056 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] ## map @@ -218,3 +222,30 @@ Spectrum of sectors, industry, country. [Source: Finviz] * -g : Data group (sector, industry or country). Default: Sector. ![cntry_spec](https://user-images.githubusercontent.com/25267873/113639067-48fd5e80-9670-11eb-95cf-0931845ddd12.png) + + +## latest + +``` +usage: latest [-i N_ID] [-n N_NUM] +``` + +Latest news articles. [Source: Seeking Alpha] +* -i : article number found on Seeking Alpha website +* -n : number of latest articles being printed. Default 10. + +latest + + +## trending + +``` +usage: trending [-i N_ID] [-n N_NUM] +``` + +Trending news articles. [Source: Seeking Alpha] +* -i : article number found on Seeking Alpha website +* -n : number of trending articles being printed. Default 10. + +trending + From bd906bcb2375538080c04540074687e1daea6681 Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 23:27:26 +0100 Subject: [PATCH 31/37] spacing --- gamestonk_terminal/discovery/disc_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index d74eaf19814..ac0ac589526 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -89,8 +89,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 [SeekingAlpha]") - print(" trending trending news [SeekingAlpha]") + print(" latest latest news [Seeking Alpha]") + print(" trending trending news [Seeking Alpha]") print("") def switch(self, an_input: str): From 96b15688759675fbd11b75c79529757c4fab2602 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 23:28:56 +0100 Subject: [PATCH 32/37] Update README.md --- gamestonk_terminal/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 2b6d79c7b2e..09b25108e07 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -79,6 +79,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/)   @@ -351,4 +353,4 @@ technical |technical (e.g. Beta, SMA50, 52W Low, RSI, Change) signals |view filter signals (e.g. -s top_gainers) -  \ No newline at end of file +  From ce6c369968d0208c719b3b143acf495cde391164 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Fri, 16 Apr 2021 23:31:42 +0100 Subject: [PATCH 33/37] Update ROADMAP.md --- ROADMAP.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ROADMAP.md b/ROADMAP.md index f5c466f3a9a..df45ed9d191 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -33,6 +33,7 @@ * [x] Top Losers (@didier) - [PR #171](https://github.com/DidierRLopes/GamestonkTerminal/pull/171) * [x] ARK orders (@aia) - [PR #140](https://github.com/DidierRLopes/GamestonkTerminal/pull/140) * [x] Analyse sectors/industry performance, overview and valuation from Finviz (@didier) - [PR #315](https://github.com/DidierRLopes/GamestonkTerminal/pull/315) +* [x] Add latest and trending news commands (@hinxx) - [PR #347](https://github.com/DidierRLopes/GamestonkTerminal/pull/347) **NEXT** From 27aa3251b03279b37464ebee1ba7c2258e853b40 Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 23:32:07 +0100 Subject: [PATCH 34/37] make lint happy --- gamestonk_terminal/discovery/seeking_alpha_model.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gamestonk_terminal/discovery/seeking_alpha_model.py b/gamestonk_terminal/discovery/seeking_alpha_model.py index 3f056b22a2e..b573c506d3f 100755 --- a/gamestonk_terminal/discovery/seeking_alpha_model.py +++ b/gamestonk_terminal/discovery/seeking_alpha_model.py @@ -128,8 +128,7 @@ def get_article_list(num: int) -> List[dict]: "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"), + ).strftime("%Y-%m-%d %H:%M:%S"), "url": "https://seekingalpha.com" + article_url, "id": article_id, } From 4a7b7402368693e39ce2167fe03c4977be4dc881 Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 23:35:32 +0100 Subject: [PATCH 35/37] flake8 leave me alone pleaaaase --- gamestonk_terminal/comparison_analysis/ca_controller.py | 1 - gamestonk_terminal/portfolio_optimization/po_controller.py | 1 - 2 files changed, 2 deletions(-) diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py index 499b6c9cd39..deedd6b8ff6 100644 --- a/gamestonk_terminal/comparison_analysis/ca_controller.py +++ b/gamestonk_terminal/comparison_analysis/ca_controller.py @@ -5,7 +5,6 @@ import argparse import requests import random from typing import List -from datetime import datetime import pandas as pd from matplotlib import pyplot as plt from gamestonk_terminal import feature_flags as gtff diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index 0913f2b50d4..6f2bb6b7bda 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -5,7 +5,6 @@ __docformat__ = "numpy" import argparse from typing import List import matplotlib.pyplot as plt -from datetime import datetime 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 f8bd6f9e6ac1651992a146ea0c6044f40972d442 Mon Sep 17 00:00:00 2001 From: didier Date: Fri, 16 Apr 2021 23:39:10 +0100 Subject: [PATCH 36/37] removed wrong import --- gamestonk_terminal/comparison_analysis/ca_controller.py | 1 + gamestonk_terminal/portfolio_optimization/po_controller.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py index deedd6b8ff6..499b6c9cd39 100644 --- a/gamestonk_terminal/comparison_analysis/ca_controller.py +++ b/gamestonk_terminal/comparison_analysis/ca_controller.py @@ -5,6 +5,7 @@ import argparse import requests import random from typing import List +from datetime import datetime import pandas as pd from matplotlib import pyplot as plt from gamestonk_terminal import feature_flags as gtff diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py index 6f2bb6b7bda..c66ca63e846 100644 --- a/gamestonk_terminal/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -10,7 +10,6 @@ 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 -from gamestonk_terminal.comparison_analysis import ca_controller class PortfolioOptimization: From d78f4d176b5b75826026d19eb4f677be63e06b88 Mon Sep 17 00:00:00 2001 From: didier Date: Sat, 17 Apr 2021 15:55:05 +0100 Subject: [PATCH 37/37] fix weird indentation from git resolve conflicts --- gamestonk_terminal/discovery/disc_controller.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index 9b5903ccc63..2362e8f762b 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -194,15 +194,15 @@ class DiscoveryController: other_args, "spectrum" ) - def call_latest(self, other_args: List[str]): + 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]): + + def call_ratings(self, other_args: List[str]): """Process ratings command""" marketbeat_view.ratings_view(other_args)