diff --git a/ROADMAP.md b/ROADMAP.md index f5c466f3a9a..86e76c6d2ac 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 top ratings updates (@hinxx) - [PR #350](https://github.com/DidierRLopes/GamestonkTerminal/pull/350) **NEXT** diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 2b6d79c7b2e..e41212591eb 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -79,6 +79,7 @@ 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) +`ratings ` |top ratings updates |[MarketBeat](https://marketbeat.com)   @@ -351,4 +352,4 @@ technical |technical (e.g. Beta, SMA50, 52W Low, RSI, Change) signals |view filter signals (e.g. -s top_gainers) -  \ No newline at end of file +  diff --git a/gamestonk_terminal/discovery/README.md b/gamestonk_terminal/discovery/README.md index 517d142d334..d1920d1975d 100644 --- a/gamestonk_terminal/discovery/README.md +++ b/gamestonk_terminal/discovery/README.md @@ -32,6 +32,8 @@ 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] +* [ratings](#ratings) + * top ratings updates [MarketBeat] ## map @@ -218,3 +220,18 @@ 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) + + +## ratings + +``` +usage: ratings [-t N_THRESHOLD] +``` + +Top ratings updates. [Source: MarketBeat] + +* -t : Minimum threshold in percentage change between current and target price to show ratings. Default: 100. + +ratings + + diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index 07bb33a3302..6125a377773 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -20,6 +20,7 @@ from gamestonk_terminal.discovery import ( spachero_view, unusual_whales_view, yahoo_finance_view, + marketbeat_view, ) @@ -46,6 +47,7 @@ class DiscoveryController: "valuation", "performance", "spectrum", + "ratings", ] def __init__(self): @@ -87,6 +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(" ratings top ratings updates [MarketBeat]") print("") def switch(self, an_input: str): @@ -187,6 +190,10 @@ class DiscoveryController: other_args, "spectrum" ) + def call_ratings(self, other_args: List[str]): + """Process ratings command""" + marketbeat_view.ratings_view(other_args) + def menu(): """Discovery Menu""" diff --git a/gamestonk_terminal/discovery/marketbeat_model.py b/gamestonk_terminal/discovery/marketbeat_model.py new file mode 100755 index 00000000000..b8bd10241e4 --- /dev/null +++ b/gamestonk_terminal/discovery/marketbeat_model.py @@ -0,0 +1,93 @@ +""" MarketBeat Model """ +__docformat__ = "numpy" + +from typing import List +import requests +from bs4 import BeautifulSoup + +from gamestonk_terminal.helper_funcs import get_user_agent + + +def get_ratings_html(url_ratings: str) -> str: + """Wraps HTTP requests.get for testibility + + Parameters + ---------- + url_ratings : str + Ratings URL + + Returns + ------- + str + HTML page of ratings + """ + ratings_html = requests.get( + url_ratings, headers={"User-Agent": get_user_agent()} + ).text + + return ratings_html + + +def get_ratings() -> List[dict]: + """Returns a list of ratings + + Parameters + ---------- + None + + Returns + ------- + list + Ratings list + """ + + ratings = list() + url_ratings = "https://www.marketbeat.com/ratings/" + + text_soup_ratings = BeautifulSoup( + get_ratings_html(url_ratings), + "lxml", + ) + + for stock_rows in text_soup_ratings.findAll("tr"): + tds = stock_rows.findAll("td") + if len(tds) == 8: + rating = {} + # company + td = tds[0] + company = td.find("div", {"class": "title-area"}).text + ticker = td.find("div", {"class": "ticker-area"}).text + rating["company"] = company + rating["ticker"] = ticker + # action + td = tds[1] + action = td.text + rating["action"] = action + # brokerage + td = tds[2] + brokerage = td.find("a").text + rating["brokerage"] = brokerage + # current price + td = tds[3] + if td.find("span"): + td.span.extract() + current_price = td.text + rating["current_price"] = current_price + # target price + td = tds[4] + target_price = td.text + rating["target_price"] = target_price + # new rating + td = tds[5] + new_rating = td.text + rating["rating"] = new_rating + # impact on price + td = tds[6] + if td.find("a"): + impact = td.find("a").text + else: + impact = td.text + rating["impact"] = impact + ratings.append(rating) + + return ratings diff --git a/gamestonk_terminal/discovery/marketbeat_view.py b/gamestonk_terminal/discovery/marketbeat_view.py new file mode 100644 index 00000000000..13ec048ee5c --- /dev/null +++ b/gamestonk_terminal/discovery/marketbeat_view.py @@ -0,0 +1,116 @@ +""" MarketBeat View """ +__docformat__ = "numpy" + +import argparse +import numpy as np +import pandas as pd +from typing import List +from gamestonk_terminal.helper_funcs import ( + check_positive, + parse_known_args_and_warn, +) + +from gamestonk_terminal.discovery import marketbeat_model + + +def ratings_view(other_args: List[str]): + """Prints top ratings updates + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-t", "100"] + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="ratings", + description="""Top ratings updates. [Source: MarketBeat]""", + ) + parser.add_argument( + "-t", + "--threshold", + action="store", + dest="n_threshold", + type=check_positive, + default=100, + help="Minimum threshold in percentage change between current and target price to show ratings", + ) + + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-t") + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + try: + ratings = marketbeat_model.get_ratings() + + df_ratings = pd.DataFrame(ratings) + df_ratings = df_ratings[df_ratings["target_price"] != ""] + df_ratings["old_target"] = df_ratings["target_price"].apply( + lambda x: x.split(" ➝ ")[0] if "➝" in x else "" + ) + df_ratings["new_target"] = df_ratings["target_price"].apply( + lambda x: x.split(" ➝ ")[1] if "➝" in x else x + ) + + df_ratings["clean_current_price"] = df_ratings["current_price"].apply( + lambda x: "".join([c if c.isdigit() or c == "." else "" for c in str(x)]) + ) + df_ratings["clean_current_price"] = df_ratings["clean_current_price"].astype( + float + ) + df_ratings["clean_target_price"] = df_ratings["new_target"].apply( + lambda x: "".join([c if c.isdigit() or c == "." else "" for c in str(x)]) + ) + df_ratings["clean_target_price"] = df_ratings["clean_target_price"].astype( + float + ) + + df_ratings["pct_increase"] = ( + 100 + * (df_ratings["clean_target_price"] - df_ratings["clean_current_price"]) + / df_ratings["clean_current_price"] + ) + df_ratings["pct_increase"] = df_ratings["pct_increase"].round(2) + + df_ratings["pct_abs"] = np.abs(df_ratings["pct_increase"]) + df_ratings["pct_abs"] = df_ratings["pct_abs"].round(2) + + df_ratings = df_ratings.sort_values("pct_abs", ascending=False) + + df_ratings_top = df_ratings[df_ratings["pct_abs"] > ns_parser.n_threshold][ + [ + "ticker", + "brokerage", + "rating", + "current_price", + "old_target", + "new_target", + "pct_increase", + ] + ] + + df_ratings_top["pct_increase"] = df_ratings_top["pct_increase"].apply( + lambda x: str(x) + " %" + ) + + print( + df_ratings_top.rename( + columns={ + "ticker": "Ticker", + "brokerage": "Brokerage", + "rating": "Rating", + "current_price": "Current Price", + "old_target": "Old Target", + "new_target": "New Target", + "pct_increase": "Percentage Increase", + } + ).to_string(index=False) + ) + + print("") + except Exception as e: + print(e, "\n")