import io import logging import os import re import uuid from typing import Any, Dict, List import disnake import financedatabase as fd import natsort import pandas as pd import plotly.graph_objects as go import yfinance as yf from kaleido.scopes.plotly import PlotlyScope from numpy.core.fromnumeric import transpose from PIL import Image from plotly.offline import plot from bots import imps from bots.groupme.groupme_helpers import send_image, send_message # pylint: disable=W0613 logger = logging.getLogger(__name__) presets_custom = [ "potential_reversals", "golden_cross_penny", "rosenwald_gtfo", "golden_cross", "bull_runs_over_10pct", "recent_growth_and_support", "heavy_inst_ins", "short_squeeze_scan", "under_15dol_stocks", "top_performers_healthcare", "oversold_under_3dol", "value_stocks", "cheap_dividend", "death_cross", "top_performers_tech", "unusual_volume", "cheap_oversold", "undervalue", "high_vol_and_low_debt", "simplistic_momentum_scanner_under_7dol", "5pct_above_low", "growth_stocks", "cheap_bottom_dividend", "analyst_strong_buy", "oversold", "rosenwald", "weak_support_and_top_performers", "channel_up_and_low_debt_and_sma_50and200", "template", "modified_neff", "buffett_like", "oversold_under_5dol", "sexy_year", "news_scanner", "top_performers_all", "stocks_strong_support_levels", "continued_momentum_scan", "modified_dreman", "break_out_stocks", ] signals = [ "top_gainers", "top_losers", "new_high", "new_low", "most_volatile", "most_active", "unusual_volume", "overbought", "oversold", "downgrades", "upgrades", "earnings_before", "earnings_after", "recent_insider_buying", "recent_insider_selling", "major_news", "horizontal_sr", "tl_resistance", "tl_support", "wedge_up", "wedge_down", "wedge", "triangle_ascending", "triangle_descending", "channel_up", "channel_down", "channel", "double_top", "double_bottom", "multiple_top", "multiple_bottom", "head_shoulders", "head_shoulders_inverse", ] metric_yf_keys = { "Return On Assets": ("financialData", "returnOnAssets"), "Return On Equity": ("financialData", "returnOnEquity"), "Current Ratio": ("financialData", "currentRatio"), "Quick Ratio": ("financialData", "quickRatio"), "Debt To Equity": ("financialData", "debtToEquity"), "Total Cash": ("financialData", "totalCash"), "Total Cash Per Share": ("financialData", "totalCashPerShare"), "Total Revenue": ("financialData", "totalRevenue"), "Revenue Per Share": ("financialData", "revenuePerShare"), "Revenue Growth": ("financialData", "revenueGrowth"), "Earnings Growth": ("financialData", "earningsGrowth"), "Profit Margins": ("financialData", "profitMargins"), "Gross Profits": ("financialData", "grossProfits"), "Gross Margins": ("financialData", "grossMargins"), "Operating Cashflow": ("financialData", "operatingCashflow"), "Operating Margins": ("financialData", "operatingMargins"), "Free Cashflow": ("financialData", "freeCashflow"), "Total Debt": ("financialData", "totalDebt"), "Earnings Before Interest, Taxes, Depreciation and Amortization": ( "financialData", "ebitda", ), "EBITDA Margins": ("financialData", "ebitdaMargins"), "Recommendation Mean": ("financialData", "recommendationMean"), "Market Cap": ("price", "marketCap"), "Full Time Employees": ("summaryProfile", "fullTimeEmployees"), "Enterprise To Revenue": ("defaultKeyStatistics", "enterpriseToRevenue"), "Book Value": ("defaultKeyStatistics", "bookValue"), "Shares Short": ("defaultKeyStatistics", "sharesShort"), "Price To Book": ("defaultKeyStatistics", "priceToBook"), "Beta": ("defaultKeyStatistics", "beta"), "Float Shares": ("defaultKeyStatistics", "floatShares"), "Short Ratio": ("defaultKeyStatistics", "shortRatio"), "Peg Ratio": ("defaultKeyStatistics", "pegRatio"), "Enterprise Value": ("defaultKeyStatistics", "enterpriseValue"), "Forward PE": ("defaultKeyStatistics", "forwardPE"), } # kaleido Scope to handle all plots image converting scope = PlotlyScope( plotlyjs=imps.bots_path.joinpath("interactive/plotly.js"), ) def load(ticker, start_date): df_stock_candidate = yf.download(ticker, start=start_date, progress=False) df_stock_candidate.index.name = "date" return df_stock_candidate def quote(ticker): ticker = yf.Ticker(ticker) quote_df = pd.DataFrame( [ { "Symbol": ticker.info["symbol"], "Name": ticker.info["shortName"], "Price": ticker.info["regularMarketPrice"], "Open": ticker.info["regularMarketOpen"], "High": ticker.info["dayHigh"], "Low": ticker.info["dayLow"], "Previous Close": ticker.info["previousClose"], "Volume": ticker.info["volume"], "52 Week High": ticker.info["fiftyTwoWeekHigh"], "52 Week Low": ticker.info["fiftyTwoWeekLow"], } ] ) quote_df["Change"] = quote_df["Price"] - quote_df["Previous Close"] quote_df["Change %"] = quote_df.apply( lambda x: f'{((x["Change"] / x["Previous Close"]) * 100):.2f}%', axis="columns", ) for c in [ "Price", "Open", "High", "Low", "Previous Close", "52 Week High", "52 Week Low", "Change", ]: quote_df[c] = quote_df[c].apply(lambda x: f"{x:.2f}") quote_df["Volume"] = quote_df["Volume"].apply(lambda x: f"{x:,}") quote_df = quote_df.set_index("Symbol") quote_data = transpose(quote_df) return quote_data def autocrop_image(image: Any, border=0) -> Any: """Crop empty space from PIL image Parameters ---------- image : Image PIL image to crop border : int, optional scale border outwards, by default 0 Returns ------- Image Cropped image """ bbox = image.getbbox() image = image.crop(bbox) (width, height) = image.size width += border * 2 height += border * 2 cropped_image = Image.new("RGBA", (width, height), (0, 0, 0, 0)) cropped_image.paste(image, (border, border)) return cropped_image conversion_mapping = { "K": 1_000, "M": 1_000_000, } all_units = "|".join(conversion_mapping.keys()) float_re = natsort.numeric_regex_chooser(natsort.ns.FLOAT | natsort.ns.SIGNED) unit_finder = re.compile(rf"({float_re})\s*({all_units})", re.IGNORECASE) def unit_replacer(matchobj): """ Given a regex match object, return a replacement string where units are modified """ number = matchobj.group(1) unit = matchobj.group(2) new_number = float(number) * conversion_mapping[unit] return f"{new_number}" def uuid_get() -> str: """Returns a UUID Returns ------- str UUID Ex. e48c4851a42711ec8e11fb53fa4c20e5 """ rand = str(uuid.uuid1()).replace("-", "") return rand def country_autocomp(inter, country: str): data = fd.show_options("equities", "countries") clow = country.lower() return [country for country in data if country.lower().startswith(clow)][:24] def industry_autocomp(inter, industry: str): data = fd.show_options("equities", "industries") if not industry: industry = "a" ilow = industry.lower() return [industry for industry in data if industry.lower().startswith(ilow)][:24] def metric_autocomp(inter, metric: str): data: dict = metric_yf_keys if not metric: data = list(data.keys()) # type: ignore return data[:24] mlow = metric.lower() return [metric for metric, _ in data.items() if metric.lower().startswith(mlow)][ :24 ] def ticker_autocomp(inter, ticker: str): if not ticker: return ["Start Typing", "for a", "stock ticker"] print(f"ticker_autocomp [ticker]: {ticker}") tlow = ticker.lower() col_list = ["Name"] file = os.path.join( os.path.dirname(os.path.abspath(__file__)), "files", "tickers.csv", ) df = pd.read_csv(file, usecols=col_list) df = df["Name"] return [ticker for ticker in df if ticker.lower().startswith(tlow)][:24] def expiry_autocomp(inter, ticker: str): data = inter.filled_options["ticker"] yf_ticker = yf.Ticker(data) dates = list(yf_ticker.options) return dates[:24] def presets_custom_autocomp(inter, preset: str): df = presets_custom if not preset: return df[:24] plow = preset.lower() print(f"preset_custom_autocomp [preset]: {preset}") return [preset for preset in df if preset.lower().startswith(plow)][:24] def signals_autocomp(inter, signal: str): df = signals if not signal: return df[:24] print(f"signal_autocomp [signal]: {signal}") slow = signal.lower() return [signal for signal in df if signal.lower().startswith(slow)][:24] def in_decreasing_color_list(df_column: pd.DataFrame.columns) -> List[str]: """Makes a colorlist for increase/decrease if value in df_column contains "-" Parameters ---------- df_column : pd.DataFrame.columns Dataframe column to create colorlist. Returns ------- List[str] List of colors for df_column """ colorlist = [ imps.PLT_CANDLE_DECREASING if boolv else imps.PLT_CANDLE_INCREASING for boolv in df_column.str.contains("-") ] return colorlist def chart_volume_scaling( df_column: pd.DataFrame.columns, range_x: int = 5 ) -> Dict[str, list]: """Takes df_column and returns volume_ticks, tickvals for chart volume scaling Parameters ---------- df_column : pd.DataFrame.columns Dataframe column range_x : int, optional Number to multiply volume, by default 5 Returns ------- Dict[str, list] {"range": volume_range, "ticks": tickvals} """ df_column = df_column.apply(lambda x: f"{x:.1f}") df_column = pd.to_numeric(df_column.astype(float)) volume_ticks = (df_column.values.max()).astype(int) round_digits = -3 first_val = round(volume_ticks * 0.20, round_digits) if len(str(volume_ticks)) > 5: round_digits = -4 first_val = round(volume_ticks * 0.20, round_digits) if len(str(volume_ticks)) > 6: round_digits = -5 first_val = round(volume_ticks * 0.20, round_digits) if len(str(volume_ticks)) > 7: round_digits = -6 first_val = round(volume_ticks * 0.20, round_digits) if len(str(volume_ticks)) > 8: round_digits = -8 first_val = round(volume_ticks * 0.20, round_digits) if len(str(volume_ticks)) > 9: round_digits = -9 first_val = round(volume_ticks * 0.20, round_digits) tickvals = [ first_val * 1, first_val * 2, first_val * 3, first_val * 4, first_val * 5, ] volume_range = [0, (volume_ticks * range_x)] return {"range": volume_range, "ticks": tickvals} def inter_chart(fig: go.Figure, filename: str, **data) -> str: """Takes plotly chart object and saves as a html file for interactive charts Parameters ---------- fig : go.Figure go.Figure chart object filename : str Name to save html as **kawrgs: config: dict = plotly config Ex. dict(scrollZoom=True, displayModeBar=False) callback: bool = enable js_callback for clickable news Returns ------- str Link for interactive charts Ex. "[Interactive]({imps.INTERACTIVE_URL}/{filename})" """ filename = f"{filename.replace('.png', '')}_{uuid_get()}.html" if "config" not in data: config = dict(scrollZoom=True, displayModeBar=False) plot_div = plot(fig, output_type="div", include_plotlyjs="plotly.js", config=config) if data["callback"]: res = re.search('