From c3a598dddeb3aa831426681ed63603b04af23d5c Mon Sep 17 00:00:00 2001 From: Hinko Kocevar Date: Fri, 16 Apr 2021 14:50:40 +0200 Subject: [PATCH 1/6] introduce latest ratings changes from MarketBeat --- .../discovery/disc_controller.py | 7 ++ .../discovery/marketbeat_model.py | 93 +++++++++++++++++++ .../discovery/marketbeat_view.py | 54 +++++++++++ 3 files changed, 154 insertions(+) create mode 100755 gamestonk_terminal/discovery/marketbeat_model.py create mode 100644 gamestonk_terminal/discovery/marketbeat_view.py diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index 07bb33a3302..de17f94334a 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 latest ratings [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..aad9b03ec5c --- /dev/null +++ b/gamestonk_terminal/discovery/marketbeat_view.py @@ -0,0 +1,54 @@ +""" MarketBeat View """ +__docformat__ = "numpy" + +import argparse +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 a list with ratings + + Parameters + ---------- + other_args : List[str] + argparse other args - ["-n", "5"] + """ + + parser = argparse.ArgumentParser( + add_help=False, + prog="ratings", + description="""Latest ratings. [Source: MarketBeat]""", + ) + + parser.add_argument( + "-n", + "--num", + action="store", + dest="n_num", + type=check_positive, + default=10, + help="number of ratings to print", + ) + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + ratings = marketbeat_model.get_ratings() + for idx, rating in enumerate(ratings): + print( + rating["ticker"], + rating["action"].replace(" by", ""), + rating["impact"], + rating["current_price"], + "->", + rating["target_price"], + ) + if idx >= ns_parser.n_num - 1: + break From 1daa6f8efbfc9f2b231eace3252de2a5ed93dace Mon Sep 17 00:00:00 2001 From: didier Date: Sat, 17 Apr 2021 01:13:35 +0100 Subject: [PATCH 2/6] Improve user experience on disc/ratings command --- .../discovery/disc_controller.py | 2 +- .../discovery/marketbeat_view.py | 100 ++++++++++++++---- 2 files changed, 82 insertions(+), 20 deletions(-) diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index de17f94334a..6125a377773 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -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(" ratings latest ratings [MarketBeat]") + print(" ratings top ratings updates [MarketBeat]") print("") def switch(self, an_input: str): diff --git a/gamestonk_terminal/discovery/marketbeat_view.py b/gamestonk_terminal/discovery/marketbeat_view.py index aad9b03ec5c..2a55f7c3479 100644 --- a/gamestonk_terminal/discovery/marketbeat_view.py +++ b/gamestonk_terminal/discovery/marketbeat_view.py @@ -2,6 +2,8 @@ __docformat__ = "numpy" import argparse +import numpy as np +import pandas as pd from typing import List from gamestonk_terminal.helper_funcs import ( check_positive, @@ -17,38 +19,98 @@ def ratings_view(other_args: List[str]): Parameters ---------- other_args : List[str] - argparse other args - ["-n", "5"] + argparse other args - ["-t", "100"] """ - parser = argparse.ArgumentParser( add_help=False, prog="ratings", description="""Latest ratings. [Source: MarketBeat]""", ) - parser.add_argument( - "-n", - "--num", + "-t", + "--threshold", action="store", - dest="n_num", + dest="n_threshold", type=check_positive, - default=10, - help="number of ratings to print", + default=100, + help="threshold to show latest ratings that show a drastic increase", ) + 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 - ratings = marketbeat_model.get_ratings() - for idx, rating in enumerate(ratings): - print( - rating["ticker"], - rating["action"].replace(" by", ""), - rating["impact"], - rating["current_price"], - "->", - rating["target_price"], + 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 "" ) - if idx >= ns_parser.n_num - 1: - break + 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") From 6c51ecfc72cb395af3171c56002adc3ce8ec4fb0 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sat, 17 Apr 2021 01:22:01 +0100 Subject: [PATCH 3/6] Update README.md --- gamestonk_terminal/discovery/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 + + From 134ea5794b293baf13476d9b7835849fbf9e6533 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sat, 17 Apr 2021 01:23:11 +0100 Subject: [PATCH 4/6] Update README.md --- gamestonk_terminal/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 +  From b430f11c87a441bfe653b9bb3242794522b37710 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sat, 17 Apr 2021 01:25:35 +0100 Subject: [PATCH 5/6] Update ROADMAP.md --- ROADMAP.md | 1 + 1 file changed, 1 insertion(+) 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** From a19db810ed4e4c139073e75f939c3d15c0719bbe Mon Sep 17 00:00:00 2001 From: didier Date: Sat, 17 Apr 2021 01:31:08 +0100 Subject: [PATCH 6/6] improve description of top ratings command --- gamestonk_terminal/discovery/marketbeat_view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gamestonk_terminal/discovery/marketbeat_view.py b/gamestonk_terminal/discovery/marketbeat_view.py index 2a55f7c3479..13ec048ee5c 100644 --- a/gamestonk_terminal/discovery/marketbeat_view.py +++ b/gamestonk_terminal/discovery/marketbeat_view.py @@ -14,7 +14,7 @@ from gamestonk_terminal.discovery import marketbeat_model def ratings_view(other_args: List[str]): - """Prints a list with ratings + """Prints top ratings updates Parameters ---------- @@ -24,7 +24,7 @@ def ratings_view(other_args: List[str]): parser = argparse.ArgumentParser( add_help=False, prog="ratings", - description="""Latest ratings. [Source: MarketBeat]""", + description="""Top ratings updates. [Source: MarketBeat]""", ) parser.add_argument( "-t", @@ -33,7 +33,7 @@ def ratings_view(other_args: List[str]): dest="n_threshold", type=check_positive, default=100, - help="threshold to show latest ratings that show a drastic increase", + help="Minimum threshold in percentage change between current and target price to show ratings", ) if other_args: