diff --git a/discordbot/run_discordbot.py b/discordbot/run_discordbot.py index f6e4ff38385..84c1da93646 100644 --- a/discordbot/run_discordbot.py +++ b/discordbot/run_discordbot.py @@ -4,7 +4,7 @@ import difflib import discord import discord_components import pyimgur -import discordbot.config_discordbot as cfg +import config_discordbot as cfg activity = discord.Game( @@ -18,7 +18,7 @@ discord_components.DiscordComponents(gst_bot) if cfg.IMGUR_CLIENT_ID == "REPLACE_ME" or cfg.DISCORD_BOT_TOKEN == "REPLACE_ME": print( - f"Update IMGUR_CLIENT_ID or DISCORD_BOT_TOKEN or both in { os.path.join('discordbot', 'config_discordot') } \n" + f"Update IMGUR_CLIENT_ID or DISCORD_BOT_TOKEN or both in { os.path.join('discordbot', 'config_discordbot') } \n" ) sys.exit() @@ -36,6 +36,7 @@ gst_bot.load_extension("stocks.dark_pool_shorts.dps_menu") gst_bot.load_extension("stocks.technical_analysis.ta_menu") gst_bot.load_extension("stocks.due_diligence.dd_menu") gst_bot.load_extension("stocks.government.gov_menu") +gst_bot.load_extension("stocks.screener.screener_menu") # Get all command names all_cmds = gst_bot.all_commands.keys() diff --git a/discordbot/stocks/__init__.py b/discordbot/stocks/__init__.py index 2df25f1bcbc..a2d546430ea 100644 --- a/discordbot/stocks/__init__.py +++ b/discordbot/stocks/__init__.py @@ -3,5 +3,6 @@ from . import ( dark_pool_shorts, due_diligence, government, + screener, technical_analysis, ) diff --git a/discordbot/stocks/screener/__init__.py b/discordbot/stocks/screener/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/discordbot/stocks/screener/financial.py b/discordbot/stocks/screener/financial.py new file mode 100644 index 00000000000..7dc2cfd2b07 --- /dev/null +++ b/discordbot/stocks/screener/financial.py @@ -0,0 +1,161 @@ +import difflib +import discord +import pandas as pd + +import discordbot.config_discordbot as cfg +from discordbot.helpers import pagination + +from gamestonk_terminal.stocks.screener.finviz_model import get_screener_data + + +async def financial_command( + ctx, preset="template", sort="", limit="25", ascend="False" +): + """Displays returned results from preset by financial metrics [Finviz]""" + try: + preset_error = False + + # Debug + if cfg.DEBUG: + print(f"!stocks.scr.financial {preset} {sort} {limit} {ascend}") + + # Check for argument + if not limit.lstrip("-").isnumeric(): + raise Exception("Number has to be an integer") + + limit = int(limit) + + if limit < 0: + raise Exception("Number has to be above 0") + + if ascend.lower() == "false": + ascend = False + elif ascend.lower() == "true": + ascend = True + else: + raise Exception("ascend argument has to be true or false") + + # Output Data + preset_error = True + df_screen = get_screener_data( + preset, + "overview", + limit, + ascend, + ) + preset_error = False + + d_cols_to_sort = { + "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", + ], + } + + description = "" + + if isinstance(df_screen, pd.DataFrame): + if df_screen.empty: + return [] + + df_screen = df_screen.dropna(axis="columns", how="all") + + if sort: + if " ".join(sort) in d_cols_to_sort["financial"]: + df_screen = df_screen.sort_values( + by=[" ".join(sort)], + ascending=ascend, + na_position="last", + ) + else: + similar_cmd = difflib.get_close_matches( + " ".join(sort), + d_cols_to_sort["financial"], + n=1, + cutoff=0.7, + ) + if similar_cmd: + description = f"Replacing '{' '.join(sort)}' by '{similar_cmd[0]}' so table can be sorted.\n\n" + df_screen = df_screen.sort_values( + by=[similar_cmd[0]], + ascending=ascend, + na_position="last", + ) + else: + raise ValueError( + f"Wrong sort column provided! Provide one of these: {', '.join(d_cols_to_sort['financial'])}" + ) + + df_screen = df_screen.fillna("") + future_column_name = df_screen["Ticker"] + df_screen = df_screen.head(n=limit).transpose() + df_screen.columns = future_column_name + df_screen.drop("Ticker") + + columns = [] + initial_str = description + "Page 0: Overview" + i = 1 + for column in df_screen.columns.values: + initial_str = initial_str + "\nPage " + str(i) + ": " + column + i += 1 + columns.append( + discord.Embed( + title="Stocks: [Finviz] Financial Screener", + description=initial_str, + colour=cfg.COLOR, + ).set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + ) + for column in df_screen.columns.values: + columns.append( + discord.Embed( + title="Stocks: [Finviz] Financial Screener", + description="```" + + df_screen[column].fillna("").to_string() + + "```", + colour=cfg.COLOR, + ).set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + ) + + await pagination(columns, ctx) + + except Exception as e: + if not preset_error: + embed = discord.Embed( + title="ERROR Stocks: [Finviz] Financial Screener", + colour=cfg.COLOR, + description=e, + ) + else: + embed = discord.Embed( + title="ERROR Stocks: [Finviz] Financial Screener", + colour=cfg.COLOR, + description=f"Wrong preset parameter entered. Use the command '{cfg.COMMAND_PREFIX}stocks.scr.presets' " + "in order to see the available presets.", + ) + embed.set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + + await ctx.send(embed=embed) diff --git a/discordbot/stocks/screener/historical.py b/discordbot/stocks/screener/historical.py new file mode 100644 index 00000000000..d8e58c98409 --- /dev/null +++ b/discordbot/stocks/screener/historical.py @@ -0,0 +1,160 @@ +import os +import random +from datetime import datetime, timedelta +import configparser +import discord +from matplotlib import pyplot as plt +import yfinance as yf +from pandas.plotting import register_matplotlib_converters +from finvizfinance.screener import ticker +import discordbot.config_discordbot as cfg + +from discordbot.run_discordbot import gst_imgur + +from gamestonk_terminal.config_plot import PLOT_DPI +from gamestonk_terminal.stocks.screener import finviz_view +from gamestonk_terminal.stocks.screener import finviz_model +from gamestonk_terminal.helper_funcs import plot_autoscale + + +async def historical_command(ctx, signal="most_volatile", start=""): + """Displays historical price comparison between similar companies [Yahoo Finance]""" + try: + + # Debug user input + if cfg.DEBUG: + print(f"!stocks.scr.historical {signal} {start}") + + register_matplotlib_converters() + + presets_path = os.path.join(cfg.GST_PATH, "stocks", "screener", "presets", "") + + screen = ticker.Ticker() + + if signal in list(finviz_model.d_signals.keys()): + screen.set_filter(signal=finviz_model.d_signals[signal]) + + else: + preset_filter = configparser.RawConfigParser() + preset_filter.optionxform = str # type: ignore + preset_filter.read(presets_path + signal + ".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: + screen.set_filter(filters_dict=d_filters) + + if start == "": + start = datetime.now() - timedelta(days=365) + else: + start = datetime.strptime(start, cfg.DATE_FORMAT) + + # Output Data + l_min = [] + l_leg = [] + l_stocks = screen.ScreenerView(verbose=0) + + if len(l_stocks) > 10: + description = ( + "\nThe limit of stocks to compare with are 10. Hence, 10 random similar stocks will be displayed." + "\nThe selected list will be: " + ) + random.shuffle(l_stocks) + l_stocks = sorted(l_stocks[:10]) + description = description + (", ".join(l_stocks)) + print(description) + + plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI) + + while l_stocks: + l_parsed_stocks = [] + for symbol in l_stocks: + try: + df_similar_stock = yf.download( + symbol, + start=datetime.strftime(start, "%Y-%m-%d"), + progress=False, + threads=False, + ) + if not df_similar_stock.empty: + plt.plot( + df_similar_stock.index, + df_similar_stock["Adj Close"].values, + ) + l_min.append(df_similar_stock.index[0]) + l_leg.append(symbol) + + l_parsed_stocks.append(symbol) + except Exception as e: + error = ( + f"{e}\nDisregard previous error, which is due to API Rate limits from Yahoo Finance. " + f"Because we like '{symbol}', and we won't leave without getting data from it." + ) + embed = discord.Embed( + title="ERROR Stocks: [Yahoo Finance] Historical Screener", + colour=cfg.COLOR, + description=error, + ) + embed.set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + + await ctx.send(embed=embed) + + for parsed_stock in l_parsed_stocks: + l_stocks.remove(parsed_stock) + + if signal: + plt.title( + f"Screener Historical Price using {finviz_view.d_signals[signal]} signal" + ) + else: + plt.title(f"Screener Historical Price using {signal} preset") + + plt.xlabel("Time") + plt.ylabel("Share Price ($)") + plt.legend(l_leg) + plt.grid(b=True, which="major", color="#666666", linestyle="-") + plt.minorticks_on() + plt.grid(b=True, which="minor", color="#999999", linestyle="-", alpha=0.2) + # ensures that the historical data starts from same datapoint + plt.xlim([max(l_min), df_similar_stock.index[-1]]) + + plt.savefig("scr_historical.png") + uploaded_image = gst_imgur.upload_image("scr_historical.png", title="something") + image_link = uploaded_image.link + if cfg.DEBUG: + print(f"Image URL: {image_link}") + title = "Stocks: [Yahoo Finance] Historical Screener" + embed = discord.Embed(title=title, description=description, colour=cfg.COLOR) + embed.set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + embed.set_image(url=image_link) + os.remove("scr_historical.png") + + await ctx.send(embed=embed) + + except Exception as e: + embed = discord.Embed( + title="ERROR Stocks: [Yahoo Finance] Historical Screener", + colour=cfg.COLOR, + description=e, + ) + embed.set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + + await ctx.send(embed=embed) diff --git a/discordbot/stocks/screener/overview.py b/discordbot/stocks/screener/overview.py new file mode 100644 index 00000000000..7e4823a054a --- /dev/null +++ b/discordbot/stocks/screener/overview.py @@ -0,0 +1,152 @@ +import difflib +import discord +import pandas as pd + +import discordbot.config_discordbot as cfg +from discordbot.helpers import pagination + +from gamestonk_terminal.stocks.screener.finviz_model import get_screener_data + + +async def overview_command(ctx, preset="template", sort="", limit="25", ascend="False"): + """Displays stocks with overview data such as Sector and Industry [Finviz]""" + try: + preset_error = False + + # Debug + if cfg.DEBUG: + print(f"!stocks.scr.overview {preset} {sort} {limit} {ascend}") + + # Check for argument + if not limit.lstrip("-").isnumeric(): + raise Exception("Number has to be an integer") + + limit = int(limit) + + if limit < 0: + raise Exception("Number has to be above 0") + + if ascend.lower() == "false": + ascend = False + elif ascend.lower() == "true": + ascend = True + else: + raise Exception("ascend argument has to be true or false") + + # Output Data + preset_error = True + df_screen = get_screener_data( + preset, + "overview", + limit, + ascend, + ) + preset_error = False + + d_cols_to_sort = { + "overview": [ + "Ticker", + "Company", + "Sector", + "Industry", + "Country", + "Market Cap", + "P/E", + "Price", + "Change", + "Volume", + ], + } + + description = "" + + if isinstance(df_screen, pd.DataFrame): + if df_screen.empty: + return [] + + df_screen = df_screen.dropna(axis="columns", how="all") + + if sort: + if " ".join(sort) in d_cols_to_sort["overview"]: + df_screen = df_screen.sort_values( + by=[" ".join(sort)], + ascending=ascend, + na_position="last", + ) + else: + similar_cmd = difflib.get_close_matches( + " ".join(sort), + d_cols_to_sort["overview"], + n=1, + cutoff=0.7, + ) + if similar_cmd: + description = f"Replacing '{' '.join(sort)}' by '{similar_cmd[0]}' so table can be sorted.\n\n" + df_screen = df_screen.sort_values( + by=[similar_cmd[0]], + ascending=ascend, + na_position="last", + ) + else: + raise ValueError( + f"Wrong sort column provided! Provide one of these: {', '.join(d_cols_to_sort['overview'])}" + ) + + df_screen = df_screen.fillna("") + future_column_name = df_screen["Ticker"] + df_screen = df_screen.head(n=limit).transpose() + df_screen.columns = future_column_name + df_screen.drop("Ticker") + + columns = [] + initial_str = description + "Page 0: Overview" + i = 1 + for column in df_screen.columns.values: + initial_str = initial_str + "\nPage " + str(i) + ": " + column + i += 1 + columns.append( + discord.Embed( + title="Stocks: [Finviz] Overview Screener", + description=initial_str, + colour=cfg.COLOR, + ).set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + ) + for column in df_screen.columns.values: + columns.append( + discord.Embed( + title="Stocks: [Finviz] Overview Screener", + description="```" + + df_screen[column].fillna("").to_string() + + "```", + colour=cfg.COLOR, + ).set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + ) + + await pagination(columns, ctx) + + except Exception as e: + if not preset_error: + embed = discord.Embed( + title="ERROR Stocks: [Finviz] Overview Screener", + colour=cfg.COLOR, + description=e, + ) + else: + embed = discord.Embed( + title="ERROR Stocks: [Finviz] Overview Screener", + colour=cfg.COLOR, + description=f"Wrong preset parameter entered. Use the command '{cfg.COMMAND_PREFIX}stocks.scr.presets' " + "in order to see the available presets.", + ) + embed.set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + + await ctx.send(embed=embed) diff --git a/discordbot/stocks/screener/ownership.py b/discordbot/stocks/screener/ownership.py new file mode 100644 index 00000000000..0efc117e9d6 --- /dev/null +++ b/discordbot/stocks/screener/ownership.py @@ -0,0 +1,158 @@ +import difflib +import discord +import pandas as pd + +import discordbot.config_discordbot as cfg +from discordbot.helpers import pagination + +from gamestonk_terminal.stocks.screener.finviz_model import get_screener_data + + +async def ownership_command( + ctx, preset="template", sort="", limit="25", ascend="False" +): + """Displays stocks based on own share float and ownership data [Finviz]""" + try: + preset_error = False + + # Debug + if cfg.DEBUG: + print(f"!stocks.scr.ownership {preset} {sort} {limit} {ascend}") + + # Check for argument + if not limit.lstrip("-").isnumeric(): + raise Exception("Number has to be an integer") + + limit = int(limit) + + if limit < 0: + raise Exception("Number has to be above 0") + + if ascend.lower() == "false": + ascend = False + elif ascend.lower() == "true": + ascend = True + else: + raise Exception("ascend argument has to be true or false") + + # Output Data + preset_error = True + df_screen = get_screener_data( + preset, + "overview", + limit, + ascend, + ) + preset_error = False + + d_cols_to_sort = { + "ownership": [ + "Ticker", + "Market Cap", + "Outstanding", + "Float", + "Insider Own", + "Insider Trans", + "Inst Own", + "Inst Trans", + "Float Short", + "Short Ratio", + "Avg Volume", + "Price", + "Change", + "Volume", + ], + } + + description = "" + + if isinstance(df_screen, pd.DataFrame): + if df_screen.empty: + return [] + + df_screen = df_screen.dropna(axis="columns", how="all") + + if sort: + if " ".join(sort) in d_cols_to_sort["ownership"]: + df_screen = df_screen.sort_values( + by=[" ".join(sort)], + ascending=ascend, + na_position="last", + ) + else: + similar_cmd = difflib.get_close_matches( + " ".join(sort), + d_cols_to_sort["ownership"], + n=1, + cutoff=0.7, + ) + if similar_cmd: + description = f"Replacing '{' '.join(sort)}' by '{similar_cmd[0]}' so table can be sorted.\n\n" + df_screen = df_screen.sort_values( + by=[similar_cmd[0]], + ascending=ascend, + na_position="last", + ) + else: + raise ValueError( + f"Wrong sort column provided! Provide one of these: {', '.join(d_cols_to_sort['ownership'])}" + ) + + df_screen = df_screen.fillna("") + future_column_name = df_screen["Ticker"] + df_screen = df_screen.head(n=limit).transpose() + df_screen.columns = future_column_name + df_screen.drop("Ticker") + + columns = [] + initial_str = description + "Page 0: Overview" + i = 1 + for column in df_screen.columns.values: + initial_str = initial_str + "\nPage " + str(i) + ": " + column + i += 1 + columns.append( + discord.Embed( + title="Stocks: [Finviz] Ownership Screener", + description=initial_str, + colour=cfg.COLOR, + ).set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + ) + for column in df_screen.columns.values: + columns.append( + discord.Embed( + title="Stocks: [Finviz] Ownership Screener", + description="```" + + df_screen[column].fillna("").to_string() + + "```", + colour=cfg.COLOR, + ).set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + ) + + await pagination(columns, ctx) + + except Exception as e: + if not preset_error: + embed = discord.Embed( + title="ERROR Stocks: [Finviz] Ownership Screener", + colour=cfg.COLOR, + description=e, + ) + else: + embed = discord.Embed( + title="ERROR Stocks: [Finviz] Ownership Screener", + colour=cfg.COLOR, + description=f"Wrong preset parameter entered. Use the command '{cfg.COMMAND_PREFIX}stocks.scr.presets' " + "in order to see the available presets.", + ) + embed.set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + + await ctx.send(embed=embed) diff --git a/discordbot/stocks/screener/performance.py b/discordbot/stocks/screener/performance.py new file mode 100644 index 00000000000..d81529f2b21 --- /dev/null +++ b/discordbot/stocks/screener/performance.py @@ -0,0 +1,160 @@ +import difflib +import discord +import pandas as pd + +import discordbot.config_discordbot as cfg +from discordbot.helpers import pagination + +from gamestonk_terminal.stocks.screener.finviz_model import get_screener_data + + +async def performance_command( + ctx, preset="template", sort="", limit="25", ascend="False" +): + """Displays stocks and sort by performance categories [Finviz]""" + try: + preset_error = False + + # Debug + if cfg.DEBUG: + print(f"!stocks.scr.performance {preset} {sort} {limit} {ascend}") + + # Check for argument + if not limit.lstrip("-").isnumeric(): + raise Exception("Number has to be an integer") + + limit = int(limit) + + if limit < 0: + raise Exception("Number has to be above 0") + + if ascend.lower() == "false": + ascend = False + elif ascend.lower() == "true": + ascend = True + else: + raise Exception("ascend argument has to be true or false") + + # Output Data + preset_error = True + df_screen = get_screener_data( + preset, + "overview", + limit, + ascend, + ) + preset_error = False + + d_cols_to_sort = { + "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", + ], + } + + description = "" + + if isinstance(df_screen, pd.DataFrame): + if df_screen.empty: + return [] + + df_screen = df_screen.dropna(axis="columns", how="all") + + if sort: + if " ".join(sort) in d_cols_to_sort["performance"]: + df_screen = df_screen.sort_values( + by=[" ".join(sort)], + ascending=ascend, + na_position="last", + ) + else: + similar_cmd = difflib.get_close_matches( + " ".join(sort), + d_cols_to_sort["performance"], + n=1, + cutoff=0.7, + ) + if similar_cmd: + description = f"Replacing '{' '.join(sort)}' by '{similar_cmd[0]}' so table can be sorted.\n\n" + df_screen = df_screen.sort_values( + by=[similar_cmd[0]], + ascending=ascend, + na_position="last", + ) + else: + raise ValueError( + "Wrong sort column provided! Provide one of these:" + f"{', '.join(d_cols_to_sort['performance'])}" + ) + + df_screen = df_screen.fillna("") + future_column_name = df_screen["Ticker"] + df_screen = df_screen.head(n=limit).transpose() + df_screen.columns = future_column_name + df_screen.drop("Ticker") + + columns = [] + initial_str = description + "Page 0: Overview" + i = 1 + for column in df_screen.columns.values: + initial_str = initial_str + "\nPage " + str(i) + ": " + column + i += 1 + columns.append( + discord.Embed( + title="Stocks: [Finviz] Performance Screener", + description=initial_str, + colour=cfg.COLOR, + ).set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + ) + for column in df_screen.columns.values: + columns.append( + discord.Embed( + title="Stocks: [Finviz] Performance Screener", + description="```" + + df_screen[column].fillna("").to_string() + + "```", + colour=cfg.COLOR, + ).set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + ) + + await pagination(columns, ctx) + + except Exception as e: + if not preset_error: + embed = discord.Embed( + title="ERROR Stocks: [Finviz] Performance Screener", + colour=cfg.COLOR, + description=e, + ) + else: + embed = discord.Embed( + title="ERROR Stocks: [Finviz] Performance Screener", + colour=cfg.COLOR, + description=f"Wrong preset parameter entered. Use the command '{cfg.COMMAND_PREFIX}stocks.scr.presets' " + "in order to see the available presets.", + ) + embed.set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + + await ctx.send(embed=embed) diff --git a/discordbot/stocks/screener/presets.py b/discordbot/stocks/screener/presets.py new file mode 100644 index 00000000000..0f0eac590e1 --- /dev/null +++ b/discordbot/stocks/screener/presets.py @@ -0,0 +1,126 @@ +import os +import discord + +import discordbot.config_discordbot as cfg +from discordbot.helpers import pagination + + +async def presets_command(ctx): + """Displays every preset""" + try: + + # Debug + if cfg.DEBUG: + print("!stocks.scr.presets") + + presets_path = os.path.join( + cfg.GST_PATH, "gamestonk_terminal", "stocks", "screener", "presets", "" + ) + + 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", + } + + presets = [ + preset.split(".")[0] + for preset in os.listdir(presets_path) + if preset[-4:] == ".ini" + ] + + description = "***Custom Presets:***\n" + + for preset in presets: + with open( + presets_path + preset + ".ini", + encoding="utf8", + ) as f: + preset_line = "" + for line in f: + if line.strip() == "[General]": + break + preset_line += line.strip() + description += f"**{preset}:** *{preset_line.split('Description: ')[1].replace('#', '')}*\n" + + description += "\n\n***Default Presets:***\n" + for signame, sigdesc in d_signals_desc.items(): + description += f"**{signame}:** *{sigdesc}*\n" + + if len(description) <= 4000: + embed = discord.Embed( + title="Stocks: Screener Presets", + description=description, + colour=cfg.COLOR, + ) + embed.set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + + await ctx.send(embed=embed) + + else: + i = 0 + str_start = 0 + str_end = 4000 + columns = [] + while i <= len(description) / 4000: + columns.append( + discord.Embed( + title="Stocks: Screener Presets", + description=description[str_start:str_end], + colour=cfg.COLOR, + ).set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + ) + str_end = str_start + str_start += 4000 + i += 1 + + await pagination(columns, ctx) + + except Exception as e: + embed = discord.Embed( + title="ERROR Stocks: Screener Presets", + colour=cfg.COLOR, + description=e, + ) + embed.set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + + await ctx.send(embed=embed) diff --git a/discordbot/stocks/screener/screener_menu.py b/discordbot/stocks/screener/screener_menu.py new file mode 100644 index 00000000000..c39c038dfdf --- /dev/null +++ b/discordbot/stocks/screener/screener_menu.py @@ -0,0 +1,286 @@ +import asyncio +import discord + +import discordbot.config_discordbot as cfg + +from discordbot.run_discordbot import gst_bot + +from discordbot.stocks.screener.presets import presets_command +from discordbot.stocks.screener.historical import historical_command +from discordbot.stocks.screener.overview import overview_command +from discordbot.stocks.screener.valuation import valuation_command +from discordbot.stocks.screener.financial import financial_command +from discordbot.stocks.screener.ownership import ownership_command +from discordbot.stocks.screener.performance import performance_command +from discordbot.stocks.screener.technical import technical_command + + +class ScreenerCommands(discord.ext.commands.Cog): + """Screener menu""" + + def __init__(self, bot: discord.ext.commands.Bot): + self.bot = bot + + @discord.ext.commands.command(name="stocks.scr.presets") + async def presets(self, ctx: discord.ext.commands.Context): + """Displays every available preset""" + await presets_command(ctx) + + @discord.ext.commands.command(name="stocks.scr.historical") + async def historical( + self, + ctx: discord.ext.commands.Context, + signal="most_volatile", + start="", + ): + """Displays trades made by the congress/senate/house [quiverquant.com] + + Parameters + ----------- + signal: str + Signal. Default: most_volatile + start: + date (in date format for start date) + """ + await historical_command(ctx, signal, start) + + @discord.ext.commands.command(name="stocks.scr.overview") + async def overview( + self, + ctx: discord.ext.commands.Context, + preset="template", + sort="", + limit="25", + ascend="False", + ): + """Displays stocks with overview data such as Sector and Industry [Finviz] + + Parameters + ----------- + preset: str + screener preset + sort: str + column to sort by + limit: int + number of stocks to display + ascend: boolean + whether it's sorted by ascending order or not. Default: False + """ + await overview_command(ctx, preset, sort, limit, ascend) + + @discord.ext.commands.command(name="stocks.scr.valuation") + async def valuation( + self, + ctx: discord.ext.commands.Context, + preset="template", + sort="", + limit="25", + ascend="False", + ): + """Displays results from chosen preset focusing on valuation metrics [Finviz] + + Parameters + ----------- + preset: str + screener preset + sort: str + column to sort by + limit: int + number of stocks to display + ascend: boolean + whether it's sorted by ascending order or not. Default: False + """ + await valuation_command(ctx, preset, sort, limit, ascend) + + @discord.ext.commands.command(name="stocks.scr.financial") + async def financial( + self, + ctx: discord.ext.commands.Context, + preset="template", + sort="", + limit="25", + ascend="False", + ): + """Displays returned results from preset by financial metrics [Finviz] + + Parameters + ----------- + preset: str + screener preset + sort: str + column to sort by + limit: int + number of stocks to display + ascend: boolean + whether it's sorted by ascending order or not. Default: False + """ + await financial_command(ctx, preset, sort, limit, ascend) + + @discord.ext.commands.command(name="stocks.scr.ownership") + async def ownership( + self, + ctx: discord.ext.commands.Context, + preset="template", + sort="", + limit="25", + ascend="False", + ): + """Displays stocks based on own share float and ownership data [Finviz] + + Parameters + ----------- + preset: str + screener preset + sort: str + column to sort by + limit: int + number of stocks to display + ascend: boolean + whether it's sorted by ascending order or not. Default: False + """ + await ownership_command(ctx, preset, sort, limit, ascend) + + @discord.ext.commands.command(name="stocks.scr.performance") + async def performance( + self, + ctx: discord.ext.commands.Context, + preset="template", + sort="", + limit="25", + ascend="False", + ): + """Displays stocks and sort by performance categories [Finviz] + + Parameters + ----------- + preset: str + screener preset + sort: str + column to sort by + limit: int + number of stocks to display + ascend: boolean + whether it's sorted by ascending order or not. Default: False + """ + await performance_command(ctx, preset, sort, limit, ascend) + + @discord.ext.commands.command(name="stocks.scr.technical") + async def technical( + self, + ctx: discord.ext.commands.Context, + preset="template", + sort="", + limit="25", + ascend="False", + ): + """Displays stocks according to chosen preset, sorting by technical factors [Finviz] + + Parameters + ----------- + preset: str + screener preset + sort: str + column to sort by + limit: int + number of stocks to display + ascend: boolean + whether it's sorted by ascending order or not. Default: False + """ + await technical_command(ctx, preset, sort, limit, ascend) + + @discord.ext.commands.command(name="stocks.scr") + async def scr(self, ctx: discord.ext.commands.Context): + """Screener Context Menu + + Returns + ------- + Sends a message to the discord user with the commands from the screener context. + The user can then select a reaction to trigger a command. + """ + + if cfg.DEBUG: + print("!stocks.scr") + + text = ( + "0️⃣ !stocks.scr.presets\n" + "1️⃣ !stocks.scr.historical \n" + "2️⃣ !stocks.scr.overview \n" + "3️⃣ !stocks.scr.valuation \n" + "4️⃣ !stocks.scr.financial \n" + "5️⃣ !stocks.scr.ownership \n" + "6️⃣ !stocks.scr.performance \n" + "7️⃣ !stocks.scr.technical " + ) + + title = "Screener Menu" + embed = discord.Embed(title=title, description=text, colour=cfg.COLOR) + embed.set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + msg = await ctx.send(embed=embed) + + emoji_list = ["0️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣"] + + for emoji in emoji_list: + await msg.add_reaction(emoji) + + def check(reaction, user): + return user == ctx.message.author and str(reaction.emoji) in emoji_list + + try: + reaction, _ = await gst_bot.wait_for( + "reaction_add", timeout=cfg.MENU_TIMEOUT, check=check + ) + if reaction.emoji == "0️⃣": + if cfg.DEBUG: + print("Reaction selected: 0") + await presets_command(ctx) + elif reaction.emoji == "1️⃣": + if cfg.DEBUG: + print("Reaction selected: 1") + await historical_command(ctx) + elif reaction.emoji == "2️⃣": + if cfg.DEBUG: + print("Reaction selected: 2") + await overview_command(ctx) + elif reaction.emoji == "3️⃣": + if cfg.DEBUG: + print("Reaction selected: 3") + await valuation_command(ctx) + elif reaction.emoji == "4️⃣": + if cfg.DEBUG: + print("Reaction selected: 4") + await financial_command(ctx) + elif reaction.emoji == "5️⃣": + if cfg.DEBUG: + print("Reaction selected: 5") + await ownership_command(ctx) + elif reaction.emoji == "6️⃣": + if cfg.DEBUG: + print("Reaction selected: 6") + await performance_command(ctx) + elif reaction.emoji == "7️⃣": + if cfg.DEBUG: + print("Reaction selected: 7") + await technical_command(ctx) + + for emoji in emoji_list: + await msg.remove_reaction(emoji, ctx.bot.user) + + except asyncio.TimeoutError: + for emoji in emoji_list: + await msg.remove_reaction(emoji, ctx.bot.user) + embed = discord.Embed( + description="Error timeout - you snooze you lose! 😋", + colour=cfg.COLOR, + title="TIMEOUT Screener Menu", + ).set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + await ctx.send(embed=embed) + + +def setup(bot: discord.ext.commands.Bot): + gst_bot.add_cog(ScreenerCommands(bot)) diff --git a/discordbot/stocks/screener/technical.py b/discordbot/stocks/screener/technical.py new file mode 100644 index 00000000000..a3c08f6f9dc --- /dev/null +++ b/discordbot/stocks/screener/technical.py @@ -0,0 +1,158 @@ +import difflib +import discord +import pandas as pd + +import discordbot.config_discordbot as cfg +from discordbot.helpers import pagination + +from gamestonk_terminal.stocks.screener.finviz_model import get_screener_data + + +async def technical_command( + ctx, preset="template", sort="", limit="25", ascend="False" +): + """Displays stocks according to chosen preset, sorting by technical factors [Finviz]""" + try: + preset_error = False + + # Debug + if cfg.DEBUG: + print(f"!stocks.scr.technical {preset} {sort} {limit} {ascend}") + + # Check for argument + if not limit.lstrip("-").isnumeric(): + raise Exception("Number has to be an integer") + + limit = int(limit) + + if limit < 0: + raise Exception("Number has to be above 0") + + if ascend.lower() == "false": + ascend = False + elif ascend.lower() == "true": + ascend = True + else: + raise Exception("ascend argument has to be true or false") + + # Output Data + preset_error = True + df_screen = get_screener_data( + preset, + "overview", + limit, + ascend, + ) + preset_error = False + + d_cols_to_sort = { + "technical": [ + "Ticker", + "Beta", + "ATR", + "SMA20", + "SMA50", + "SMA200", + "52W High", + "52W Low", + "RSI", + "Price", + "Change", + "from Open", + "Gap", + "Volume", + ], + } + + description = "" + + if isinstance(df_screen, pd.DataFrame): + if df_screen.empty: + return [] + + df_screen = df_screen.dropna(axis="columns", how="all") + + if sort: + if " ".join(sort) in d_cols_to_sort["technical"]: + df_screen = df_screen.sort_values( + by=[" ".join(sort)], + ascending=ascend, + na_position="last", + ) + else: + similar_cmd = difflib.get_close_matches( + " ".join(sort), + d_cols_to_sort["technical"], + n=1, + cutoff=0.7, + ) + if similar_cmd: + description = f"Replacing '{' '.join(sort)}' by '{similar_cmd[0]}' so table can be sorted.\n\n" + df_screen = df_screen.sort_values( + by=[similar_cmd[0]], + ascending=ascend, + na_position="last", + ) + else: + raise ValueError( + f"Wrong sort column provided! Provide one of these: {', '.join(d_cols_to_sort['technical'])}" + ) + + df_screen = df_screen.fillna("") + future_column_name = df_screen["Ticker"] + df_screen = df_screen.head(n=limit).transpose() + df_screen.columns = future_column_name + df_screen.drop("Ticker") + + columns = [] + initial_str = description + "Page 0: Overview" + i = 1 + for column in df_screen.columns.values: + initial_str = initial_str + "\nPage " + str(i) + ": " + column + i += 1 + columns.append( + discord.Embed( + title="Stocks: [Finviz] Technical Screener", + description=initial_str, + colour=cfg.COLOR, + ).set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + ) + for column in df_screen.columns.values: + columns.append( + discord.Embed( + title="Stocks: [Finviz] Technical Screener", + description="```" + + df_screen[column].fillna("").to_string() + + "```", + colour=cfg.COLOR, + ).set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + ) + + await pagination(columns, ctx) + + except Exception as e: + if not preset_error: + embed = discord.Embed( + title="ERROR Stocks: [Finviz] Technical Screener", + colour=cfg.COLOR, + description=e, + ) + else: + embed = discord.Embed( + title="ERROR Stocks: [Finviz] Technical Screener", + colour=cfg.COLOR, + description=f"Wrong preset parameter entered. Use the command '{cfg.COMMAND_PREFIX}stocks.scr.presets' " + "in order to see the available presets.", + ) + embed.set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + + await ctx.send(embed=embed) diff --git a/discordbot/stocks/screener/valuation.py b/discordbot/stocks/screener/valuation.py new file mode 100644 index 00000000000..143446a0b85 --- /dev/null +++ b/discordbot/stocks/screener/valuation.py @@ -0,0 +1,161 @@ +import difflib +import discord +import pandas as pd + +import discordbot.config_discordbot as cfg +from discordbot.helpers import pagination + +from gamestonk_terminal.stocks.screener.finviz_model import get_screener_data + + +async def valuation_command( + ctx, preset="template", sort="", limit="25", ascend="False" +): + """Displays results from chosen preset focusing on valuation metrics [Finviz]""" + try: + preset_error = False + + # Debug + if cfg.DEBUG: + print(f"!stocks.scr.valuation {preset} {sort} {limit} {ascend}") + + # Check for argument + if not limit.lstrip("-").isnumeric(): + raise Exception("Number has to be an integer") + + limit = int(limit) + + if limit < 0: + raise Exception("Number has to be above 0") + + if ascend.lower() == "false": + ascend = False + elif ascend.lower() == "true": + ascend = True + else: + raise Exception("ascend argument has to be true or false") + + # Output Data + preset_error = True + df_screen = get_screener_data( + preset, + "overview", + limit, + ascend, + ) + preset_error = False + + d_cols_to_sort = { + "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", + ], + } + + description = "" + + if isinstance(df_screen, pd.DataFrame): + if df_screen.empty: + return [] + + df_screen = df_screen.dropna(axis="columns", how="all") + + if sort: + if " ".join(sort) in d_cols_to_sort["valuation"]: + df_screen = df_screen.sort_values( + by=[" ".join(sort)], + ascending=ascend, + na_position="last", + ) + else: + similar_cmd = difflib.get_close_matches( + " ".join(sort), + d_cols_to_sort["valuation"], + n=1, + cutoff=0.7, + ) + if similar_cmd: + description = f"Replacing '{' '.join(sort)}' by '{similar_cmd[0]}' so table can be sorted.\n\n" + df_screen = df_screen.sort_values( + by=[similar_cmd[0]], + ascending=ascend, + na_position="last", + ) + else: + raise ValueError( + f"Wrong sort column provided! Provide one of these: {', '.join(d_cols_to_sort['valuation'])}" + ) + + df_screen = df_screen.fillna("") + future_column_name = df_screen["Ticker"] + df_screen = df_screen.head(n=limit).transpose() + df_screen.columns = future_column_name + df_screen.drop("Ticker") + + columns = [] + initial_str = description + "Page 0: Overview" + i = 1 + for column in df_screen.columns.values: + initial_str = initial_str + "\nPage " + str(i) + ": " + column + i += 1 + columns.append( + discord.Embed( + title="Stocks: [Finviz] Valuation Screener", + description=initial_str, + colour=cfg.COLOR, + ).set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + ) + for column in df_screen.columns.values: + columns.append( + discord.Embed( + title="Stocks: [Finviz] Valuation Screener", + description="```" + + df_screen[column].fillna("").to_string() + + "```", + colour=cfg.COLOR, + ).set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + ) + + await pagination(columns, ctx) + + except Exception as e: + if not preset_error: + embed = discord.Embed( + title="ERROR Stocks: [Finviz] Valuation Screener", + colour=cfg.COLOR, + description=e, + ) + else: + embed = discord.Embed( + title="ERROR Stocks: [Finviz] Valuation Screener", + colour=cfg.COLOR, + description=f"Wrong preset parameter entered. Use the command '{cfg.COMMAND_PREFIX}stocks.scr.presets' " + "in order to see the available presets.", + ) + embed.set_author( + name=cfg.AUTHOR_NAME, + icon_url=cfg.AUTHOR_ICON_URL, + ) + + await ctx.send(embed=embed)