diff --git a/gamestonk_terminal/stocks/screener/finviz_model.py b/gamestonk_terminal/stocks/screener/finviz_model.py index 079ec1054a4..62f81c241de 100644 --- a/gamestonk_terminal/stocks/screener/finviz_model.py +++ b/gamestonk_terminal/stocks/screener/finviz_model.py @@ -14,99 +14,6 @@ presets_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "presets # pylint: disable=C0302 - -def get_screener_data( - preset_loaded: str, data_type: str, signal: str, limit: int, ascend: bool -): - """Screener Overview - - Parameters - ---------- - preset_loaded : str - Loaded preset filter - data_type : str - Data type between: overview, valuation, financial, ownership, performance, technical - signal : str - Signal to use to filter data - limit : int - Limit of stocks filtered with presets to print - ascend : bool - Ascended order of stocks filtered to print - - Returns - ---------- - pd.DataFrame - Dataframe with loaded filtered stocks - """ - preset_filter = configparser.RawConfigParser() - preset_filter.optionxform = str # type: ignore - preset_filter.read(presets_path + preset_loaded + ".ini") - - d_general = preset_filter["General"] - d_filters = { - **preset_filter["Descriptive"], - **preset_filter["Fundamental"], - **preset_filter["Technical"], - } - - for section in ["General", "Descriptive", "Fundamental", "Technical"]: - for key, val in {**preset_filter[section]}.items(): - if key not in d_check_screener: - print(f"The screener variable {section}.{key} shouldn't exist!\n") - return pd.DataFrame() - - if val not in d_check_screener[key]: - print( - f"Invalid [{section}] {key}={val}. " - f"Choose one of the following options:\n{', '.join(d_check_screener[key])}.\n" - ) - return pd.DataFrame() - - d_filters = {k: v for k, v in d_filters.items() if v} - - if data_type == "overview": - screen = overview.Overview() - elif data_type == "valuation": - screen = valuation.Valuation() - elif data_type == "financial": - screen = financial.Financial() - elif data_type == "ownership": - screen = ownership.Ownership() - elif data_type == "performance": - screen = performance.Performance() - elif data_type == "technical": - screen = technical.Technical() - else: - print("Invalid selected screener type") - return pd.DataFrame() - - if signal: - screen.set_filter(signal=d_signals[signal]) - else: - if "Signal" in d_general: - screen.set_filter(filters_dict=d_filters, signal=d_general["Signal"]) - else: - screen.set_filter(filters_dict=d_filters) - - if "Order" in d_general: - if limit > 0: - df_screen = screen.ScreenerView( - order=d_general["Order"], - limit=limit, - ascend=ascend, - ) - else: - df_screen = screen.ScreenerView(order=d_general["Order"], ascend=ascend) - - else: - if limit > 0: - df_screen = screen.ScreenerView(limit=limit, ascend=ascend) - else: - df_screen = screen.ScreenerView(ascend=ascend) - - return df_screen - - d_signals = { "top_gainers": "Top Gainers", "top_losers": "Top Losers", @@ -143,6 +50,135 @@ d_signals = { "head_shoulders_inverse": "Head & Shoulders Inverse", } + +def get_screener_data(preset_loaded: str, data_type: str, limit: int, ascend: bool): + """Screener Overview + + Parameters + ---------- + preset_loaded : str + Loaded preset filter + data_type : str + Data type between: overview, valuation, financial, ownership, performance, technical + limit : int + Limit of stocks filtered with presets to print + ascend : bool + Ascended order of stocks filtered to print + + Returns + ---------- + pd.DataFrame + Dataframe with loaded filtered stocks + """ + if data_type == "overview": + screen = overview.Overview() + elif data_type == "valuation": + screen = valuation.Valuation() + elif data_type == "financial": + screen = financial.Financial() + elif data_type == "ownership": + screen = ownership.Ownership() + elif data_type == "performance": + screen = performance.Performance() + elif data_type == "technical": + screen = technical.Technical() + else: + print("Invalid selected screener type") + return pd.DataFrame() + + if preset_loaded in list(d_signals.keys()): + screen.set_filter(signal=d_signals[preset_loaded]) + + if limit > 0: + df_screen = screen.ScreenerView(limit=limit, ascend=ascend) + else: + df_screen = screen.ScreenerView(ascend=ascend) + + else: + preset_filter = configparser.RawConfigParser() + preset_filter.optionxform = str # type: ignore + preset_filter.read(presets_path + preset_loaded + ".ini") + + d_general = preset_filter["General"] + d_filters = { + **preset_filter["Descriptive"], + **preset_filter["Fundamental"], + **preset_filter["Technical"], + } + + for section in ["General", "Descriptive", "Fundamental", "Technical"]: + for key, val in {**preset_filter[section]}.items(): + if key not in d_check_screener: + print(f"The screener variable {section}.{key} shouldn't exist!\n") + return pd.DataFrame() + + if val not in d_check_screener[key]: + print( + f"Invalid [{section}] {key}={val}. " + f"Choose one of the following options:\n{', '.join(d_check_screener[key])}.\n" + ) + return pd.DataFrame() + + d_filters = {k: v for k, v in d_filters.items() if v} + + screen.set_filter(filters_dict=d_filters) + + if "Order" in d_general: + if limit > 0: + df_screen = screen.ScreenerView( + order=d_general["Order"], + limit=limit, + ascend=ascend, + ) + else: + df_screen = screen.ScreenerView(order=d_general["Order"], ascend=ascend) + + else: + if limit > 0: + df_screen = screen.ScreenerView(limit=limit, ascend=ascend) + else: + df_screen = screen.ScreenerView(ascend=ascend) + + return df_screen + + +d_signals_desc = { + "top_gainers": "stocks with the highest %% price gain today", + "top_losers": "stocks with the highest %% price loss today", + "new_high": "stocks making 52-week high today", + "new_low": "stocks making 52-week low today", + "most_volatile": "stocks with the highest widest high/low trading range today", + "most_active": "stocks with the highest trading volume today", + "unusual_volume": "stocks with unusually high volume today - the highest relative volume ratio", + "overbought": "stock is becoming overvalued and may experience a pullback.", + "oversold": "oversold stocks may represent a buying opportunity for investors", + "downgrades": "stocks downgraded by analysts today", + "upgrades": "stocks upgraded by analysts today", + "earnings_before": "companies reporting earnings today, before market open", + "earnings_after": "companies reporting earnings today, after market close", + "recent_insider_buying": "stocks with recent insider buying activity", + "recent_insider_selling": "stocks with recent insider selling activity", + "major_news": "stocks with the highest news coverage today", + "horizontal_sr": "horizontal channel of price range between support and resistance trendlines", + "tl_resistance": "once a rising trendline is broken", + "tl_support": "once a falling trendline is broken", + "wedge_up": "upward trendline support and upward trendline resistance (reversal)", + "wedge_down": "downward trendline support and downward trendline resistance (reversal)", + "wedge": "upward trendline support, downward trendline resistance (contiunation)", + "triangle_ascending": "upward trendline support and horizontal trendline resistance", + "triangle_descending": "horizontal trendline support and downward trendline resistance", + "channel_up": "both support and resistance trendlines slope upward", + "channel_down": "both support and resistance trendlines slope downward", + "channel": "both support and resistance trendlines are horizontal", + "double_top": "stock with 'M' shape that indicates a bearish reversal in trend", + "double_bottom": "stock with 'W' shape that indicates a bullish reversal in trend", + "multiple_top": "same as double_top hitting more highs", + "multiple_bottom": "same as double_bottom hitting more lows", + "head_shoulders": "chart formation that predicts a bullish-to-bearish trend reversal", + "head_shoulders_inverse": "chart formation that predicts a bearish-to-bullish trend reversal", +} + + d_check_screener = { "Order": [ "Any", diff --git a/gamestonk_terminal/stocks/screener/finviz_view.py b/gamestonk_terminal/stocks/screener/finviz_view.py index a5f617bb3f5..8112c3134ee 100644 --- a/gamestonk_terminal/stocks/screener/finviz_view.py +++ b/gamestonk_terminal/stocks/screener/finviz_view.py @@ -4,19 +4,126 @@ __docformat__ = "numpy" import argparse from typing import List import os -from datetime import datetime +import difflib +from tabulate import tabulate import pandas as pd from gamestonk_terminal.helper_funcs import ( parse_known_args_and_warn, check_positive, + try_except, + export_data, ) from gamestonk_terminal.stocks.screener.finviz_model import ( d_signals, presets_path, get_screener_data, ) +from gamestonk_terminal import feature_flags as gtff + +d_cols_to_sort = { + "overview": [ + "Ticker", + "Company", + "Sector", + "Industry", + "Country", + "Market Cap", + "P/E", + "Price", + "Change", + "Volume", + ], + "valuation": [ + "Ticker", + "Market Cap", + "P/E", + "Fwd P/E", + "PEG", + "P/S", + "P/B", + "P/C", + "P/FCF", + "EPS this Y", + "EPS next Y", + "EPS past 5Y", + "EPS next 5Y", + "Sales past 5Y", + "Price", + "Change", + "Volume", + ], + "financial": [ + "Ticker", + "Market Cap", + "Dividend", + "ROA", + "ROE", + "ROI", + "Curr R", + "Quick R", + "LTDebt/Eq", + "Debt/Eq", + "Gross M", + "Oper M", + "Profit M", + "Earnings", + "Price", + "Change", + "Volume", + ], + "ownership": [ + "Ticker", + "Market Cap", + "Outstanding", + "Float", + "Insider Own", + "Insider Trans", + "Inst Own", + "Inst Trans", + "Float Short", + "Short Ratio", + "Avg Volume", + "Price", + "Change", + "Volume", + ], + "performance": [ + "Ticker", + "Perf Week", + "Perf Month", + "Perf Quart", + "Perf Half", + "Perf Year", + "Perf YTD", + "Volatility W", + "Volatility M", + "Recom", + "Avg Volume", + "Rel Volume", + "Price", + "Change", + "Volume", + ], + "technical": [ + "Ticker", + "Beta", + "ATR", + "SMA20", + "SMA50", + "SMA200", + "52W High", + "52W Low", + "RSI", + "Price", + "Change", + "from Open", + "Gap", + "Volume", + ], +} +@try_except def screener(other_args: List[str], loaded_preset: str, data_type: str) -> List[str]: """Screener @@ -56,17 +163,8 @@ def screener(other_args: List[str], loaded_preset: str, data_type: str) -> List[ preset.split(".")[0] for preset in os.listdir(presets_path) if preset[-4:] == ".ini" - ], - ) - parser.add_argument( - "-s", - "--signal", - action="store", - dest="signal", - type=str, - default=None, - help="Signal", - choices=list(d_signals.keys()), + ] + + list(d_signals.keys()), ) parser.add_argument( "-l", @@ -74,7 +172,7 @@ def screener(other_args: List[str], loaded_preset: str, data_type: str) -> List[ action="store", dest="limit", type=check_positive, - default=0, + default=10, help="Limit of stocks to print", ) parser.add_argument( @@ -86,147 +184,91 @@ def screener(other_args: List[str], loaded_preset: str, data_type: str) -> List[ help="Set order to Ascend, the default is Descend", ) parser.add_argument( - "-e", + "-s", + "--sort", + action="store", + dest="sort", + default="", + nargs="+", + help=f"Sort elements of the table. Use {', '.join(d_cols_to_sort[data_type])}", + ) + parser.add_argument( "--export", - action="store_true", - dest="exportFile", - help="Save list as a text file", + choices=["csv", "json", "xlsx"], + default="", + dest="export", + help="Export dataframe data to csv,json,xlsx file", ) - try: - ns_parser = parse_known_args_and_warn(parser, other_args) - if not ns_parser: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return [] + + df_screen = get_screener_data( + ns_parser.preset, + data_type, + 0, + ns_parser.ascend, + ) + + if isinstance(df_screen, pd.DataFrame): + if df_screen.empty: return [] - df_screen = get_screener_data( - ns_parser.preset, + df_screen = df_screen.dropna(axis="columns", how="all") + + if ns_parser.sort: + if " ".join(ns_parser.sort) in d_cols_to_sort[data_type]: + df_screen = df_screen.sort_values( + by=[" ".join(ns_parser.sort)], + ascending=ns_parser.ascend, + na_position="last", + ) + else: + similar_cmd = difflib.get_close_matches( + " ".join(ns_parser.sort), + d_cols_to_sort[data_type], + n=1, + cutoff=0.7, + ) + if similar_cmd: + print( + f"Replacing '{' '.join(ns_parser.sort)}' by '{similar_cmd[0]}' so table can be sorted." + ) + df_screen = df_screen.sort_values( + by=[similar_cmd[0]], + ascending=ns_parser.ascend, + na_position="last", + ) + else: + print( + f"Wrong sort column provided! Provide one of these: {', '.join(d_cols_to_sort[data_type])}" + ) + + df_screen = df_screen.fillna("") + + if gtff.USE_TABULATE_DF: + print( + tabulate( + df_screen.head(n=ns_parser.limit), + headers=df_screen.columns, + floatfmt=".2f", + showindex=False, + tablefmt="fancy_grid", + ), + ) + else: + print(df_screen.head(n=ns_parser.limit).to_string()) + print("") + + export_data( + ns_parser.export, + os.path.dirname(os.path.abspath(__file__)), data_type, - ns_parser.signal, - ns_parser.limit, - ns_parser.ascend, + df_screen, ) - if isinstance(df_screen, pd.DataFrame): - if df_screen.empty: - return [] + return list(df_screen.head(n=ns_parser.limit)["Ticker"].values) - print(df_screen.to_string()) - print("") - if ns_parser.exportFile: - now = datetime.now() - if not os.path.exists("reports/screener"): - os.makedirs("reports/screener") - with open( - f"reports/screener/{ns_parser.signal}-{now.strftime('%Y-%m-%d_%H:%M:%S')}", - "w", - ) as file: - file.write(df_screen.to_string(index=False) + "\n") - - return list(df_screen["Ticker"].values) - - print("") - return [] - - except Exception as e: - print(e, "\n") - return [] - - -def view_signals(other_args: List[str]): - """View list of available signals - - Parameters - ---------- - other_args : List[str] - Command line arguments to be processed with argparse - """ - parser = argparse.ArgumentParser( - add_help=False, - prog="signals", - description=""" - Prints list of available signals. [Source: Finviz] - """, - ) - - try: - ns_parser = parse_known_args_and_warn(parser, other_args) - if not ns_parser: - return - - print("top_gainers stocks with the highest %% price gain today") - print("top_losers stocks with the highest %% price loss today.") - print("new_high stocks making 52-week high today") - print("new_low stocks making 52-week low today") - print( - "most_volatile stocks with the highest widest high/low trading range today" - ) - print("most_active stocks with the highest trading volume today") - print( - "unusual_volume stocks with unusually high volume today - the highest relative volume ratio" - ) - print( - "overbought stock is becoming overvalued and may experience a pullback." - ) - print( - "oversold oversold stocks may represent a buying opportunity for investors." - ) - print("downgrades stocks downgraded by analysts today") - print("upgrades stocks upgraded by analysts today.") - print( - "earnings_before companies reporting earnings today, before market open" - ) - print( - "earnings_after companies reporting earnings today, after market close" - ) - print("recent_insider_buying stocks with recent insider buying activity") - print("recent_insider_selling stocks with recent insider selling activity") - print("major_news stocks with the highest news coverage today") - print( - "horizontal_sr horizontal channel of price range between support and resistance trendlines" - ) - print("tl_resistance once a rising trendline is broken") - print("tl_support once a falling trendline is broken") - print( - "wedge_up upward trendline support and upward trendline resistance (reversal)" - ) - print( - "wedge_down downward trendline support and downward trendline resistance (reversal)" - ) - print( - "wedge upward trendline support, downward trendline resistance (contiunation)" - ) - print( - "triangle_ascending upward trendline support and horizontal trendline resistance" - ) - print( - "triangle_descending horizontal trendline support and downward trendline resistance" - ) - print( - "channel_up both support and resistance trendlines slope upward" - ) - print( - "channel_down both support and resistance trendlines slope downward" - ) - print( - "channel both support and resistance trendlines are horizontal" - ) - print( - "double_top stock with 'M' shape that indicates a bearish reversal in trend" - ) - print( - "double_bottom stock with 'W' shape that indicates a bullish reversal in trend" - ) - print("multiple_top same as double_top hitting more highs") - print("multiple_bottom same as double_bottom hitting more lows") - print( - "head_shoulders chart formation that predicts a bullish-to-bearish trend reversal" - ) - print( - "head_shoulders_inverse chart formation that predicts a bearish-to-bullish trend reversal" - ) - print("") - - except Exception as e: - print(e) - print("") - return + print("") + return [] diff --git a/gamestonk_terminal/stocks/screener/screener_controller.py b/gamestonk_terminal/stocks/screener/screener_controller.py index 556c0be2e3a..0c48d550fef 100644 --- a/gamestonk_terminal/stocks/screener/screener_controller.py +++ b/gamestonk_terminal/stocks/screener/screener_controller.py @@ -17,7 +17,11 @@ from gamestonk_terminal.helper_funcs import ( ) from gamestonk_terminal.menu import session from gamestonk_terminal.portfolio.portfolio_optimization import po_controller -from gamestonk_terminal.stocks.screener import finviz_view, yahoofinance_view +from gamestonk_terminal.stocks.screener import ( + finviz_view, + yahoofinance_view, + finviz_model, +) presets_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "presets/") @@ -43,13 +47,12 @@ class ScreenerController: "ownership", "performance", "technical", - "signals", "po", ] def __init__(self): """Constructor""" - self.preset = "template" + self.preset = "top_gainers" self.screen_tickers = [] self.scr_parser = argparse.ArgumentParser(add_help=False, prog="scr") self.scr_parser.add_argument( @@ -65,7 +68,7 @@ class ScreenerController: print(" q quit this menu, and shows back to main menu") print(" quit quit to abandon program") print("") - print(" view view available presets") + print(" view view available presets (defaults and customs)") print(" set set one of the available presets") print("") print(f"PRESET: {self.preset}") @@ -78,8 +81,6 @@ class ScreenerController: print(" performance performance (e.g. Perf Week, Perf YTD, Volatility M)") print(" technical technical (e.g. Beta, SMA50, 52W Low, RSI, Change)") print("") - print(" signals view filter signals (e.g. -s top_gainers)") - print("") if self.screen_tickers: print(f"Last screened tickers: {', '.join(self.screen_tickers)}") print("") @@ -142,6 +143,7 @@ class ScreenerController: if preset[-4:] == ".ini" ] + print("\nCustom Presets:") for preset in presets: with open( presets_path + preset + ".ini", @@ -152,8 +154,13 @@ class ScreenerController: if line.strip() == "[General]": break description += line.strip() - print(f"\nPRESET: {preset}") - print(description.split("Description: ")[1].replace("#", "")) + print( + f" {preset}{(50-len(preset)) * ' '}{description.split('Description: ')[1].replace('#', '')}" + ) + + print("\nDefault Presets:") + for signame, sigdesc in finviz_model.d_signals_desc.items(): + print(f" {signame}{(50-len(signame)) * ' '}{sigdesc}") print("") except Exception as e: @@ -178,7 +185,8 @@ class ScreenerController: preset.split(".")[0] for preset in os.listdir(presets_path) if preset[-4:] == ".ini" - ], + ] + + list(finviz_model.d_signals.keys()), ) try: @@ -280,10 +288,6 @@ class ScreenerController: """Process technical command""" 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, _): """Call the portfolio optimization menu with selected tickers""" return po_controller.menu(self.screen_tickers) diff --git a/gamestonk_terminal/stocks/screener/yahoofinance_view.py b/gamestonk_terminal/stocks/screener/yahoofinance_view.py index 262fa3fbf34..b2cfd058731 100644 --- a/gamestonk_terminal/stocks/screener/yahoofinance_view.py +++ b/gamestonk_terminal/stocks/screener/yahoofinance_view.py @@ -18,7 +18,7 @@ from gamestonk_terminal.helper_funcs import ( plot_autoscale, valid_date, ) -from gamestonk_terminal.stocks.screener import finviz_view +from gamestonk_terminal.stocks.screener import finviz_model register_matplotlib_converters() @@ -76,40 +76,30 @@ def historical(other_args: List[str], preset_loaded: str) -> List[str]: default="a", # in case it's adjusted close help=("type of candles: o-open, h-high, l-low, c-close, a-adjusted close."), ) - parser.add_argument( - "-s", - "--signal", - action="store", - dest="signal", - type=str, - default=None, - help="Signal", - choices=list(finviz_view.d_signals.keys()), - ) try: ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return list() - preset_filter = configparser.RawConfigParser() - preset_filter.optionxform = str # type: ignore - preset_filter.read(presets_path + preset_loaded + ".ini") - - d_general = preset_filter["General"] - d_filters = { - **preset_filter["Descriptive"], - **preset_filter["Fundamental"], - **preset_filter["Technical"], - } - - d_filters = {k: v for k, v in d_filters.items() if v} - screen = ticker.Ticker() + if preset_loaded in list(finviz_model.d_signals.keys()): + screen.set_filter(signal=finviz_model.d_signals[preset_loaded]) - if ns_parser.signal: - screen.set_filter(signal=finviz_view.d_signals[ns_parser.signal]) else: + preset_filter = configparser.RawConfigParser() + preset_filter.optionxform = str # type: ignore + preset_filter.read(presets_path + preset_loaded + ".ini") + + d_general = preset_filter["General"] + d_filters = { + **preset_filter["Descriptive"], + **preset_filter["Fundamental"], + **preset_filter["Technical"], + } + + d_filters = {k: v for k, v in d_filters.items() if v} + if d_general["Signal"]: screen.set_filter(filters_dict=d_filters, signal=d_general["Signal"]) else: @@ -117,9 +107,8 @@ def historical(other_args: List[str], preset_loaded: str) -> List[str]: l_min = [] l_leg = [] - plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI) - l_stocks = screen.ScreenerView(verbose=0) + limit_random_stocks = False if len(l_stocks) > 10: print( @@ -129,6 +118,9 @@ def historical(other_args: List[str], preset_loaded: str) -> List[str]: random.shuffle(l_stocks) l_stocks = sorted(l_stocks[:10]) print(", ".join(l_stocks)) + limit_random_stocks = True + + plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI) while l_stocks: l_parsed_stocks = [] @@ -164,12 +156,12 @@ def historical(other_args: List[str], preset_loaded: str) -> List[str]: for parsed_stock in l_parsed_stocks: l_stocks.remove(parsed_stock) - if ns_parser.signal: + if limit_random_stocks: plt.title( - f"Screener Historical Price using {finviz_view.d_signals[ns_parser.signal]} signal" + f"Screener Historical Price using {preset_loaded} on 10 of those stocks" ) else: - plt.title(f"Screener Historical Price using {preset_loaded} preset") + plt.title(f"Screener Historical Price using {preset_loaded}") plt.xlabel("Time") plt.ylabel("Share Price ($)")