diff --git a/Dockerfile b/Dockerfile index d69a610..c3ef0a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8-buster +FROM python:3.9-buster diff --git a/IEX_Symbol.py b/IEX_Symbol.py new file mode 100644 index 0000000..b29c6f3 --- /dev/null +++ b/IEX_Symbol.py @@ -0,0 +1,423 @@ +"""Class with functions for running the bot with IEX Cloud. +""" + +from datetime import datetime +from typing import Optional, List, Tuple + +import pandas as pd +import requests as r +import schedule +from fuzzywuzzy import fuzz +import os + +from Symbol import Stock + + +class IEX_Symbol: + """ + Functions for finding stock market information about symbols. + """ + + SYMBOL_REGEX = "[$]([a-zA-Z]{1,4})" + + searched_symbols = {} + charts = {} + + def __init__(self) -> None: + """Creates a Symbol Object + + Parameters + ---------- + IEX_TOKEN : str + IEX Token + """ + try: + self.IEX_TOKEN = os.environ["IEX"] + except KeyError: + self.IEX_TOKEN = "" + print( + "Starting without an IEX Token will not allow you to get market data!" + ) + + if self.IEX_TOKEN != "": + self.get_symbol_list() + + schedule.every().day.do(self.get_symbol_list) + schedule.every().day.do(self.clear_charts) + + def clear_charts(self) -> None: + """Clears cache of chart data.""" + self.charts = {} + + def get_symbol_list( + self, return_df=False + ) -> Optional[Tuple[pd.DataFrame, datetime]]: + + raw_symbols = r.get( + f"https://cloud.iexapis.com/stable/ref-data/symbols?token={self.IEX_TOKEN}" + ).json() + symbols = pd.DataFrame(data=raw_symbols) + + symbols["description"] = symbols["symbol"] + ": " + symbols["name"] + self.symbol_list = symbols + if return_df: + return symbols, datetime.now() + + def status(self) -> str: + """Checks IEX Status dashboard for any current API issues. + + Returns + ------- + str + Human readable text on status of IEX API + """ + resp = r.get("https://pjmps0c34hp7.statuspage.io/api/v2/status.json") + + if resp.status_code == 200: + status = resp.json()["status"] + else: + return "IEX Cloud did not respond. Please check their status page for more information. https://status.iexapis.com" + + if status["indicator"] == "none": + return "IEX Cloud is currently not reporting any issues with its API." + else: + return ( + f"{status['indicator']}: {status['description']}." + + " Please check the status page for more information. https://status.iexapis.com" + ) + + def search_symbols(self, search: str) -> List[Tuple[str, str]]: + """Performs a fuzzy search to find stock symbols closest to a search term. + + Parameters + ---------- + search : str + String used to search, could be a company name or something close to the companies stock ticker. + + Returns + ------- + List[tuple[str, str]] + A list tuples of every stock sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name). + """ + + schedule.run_pending() + search = search.lower() + try: # https://stackoverflow.com/a/3845776/8774114 + return self.searched_symbols[search] + except KeyError: + pass + + symbols = self.symbol_list + symbols["Match"] = symbols.apply( + lambda x: fuzz.ratio(search, f"{x['symbol']}".lower()), + axis=1, + ) + + symbols.sort_values(by="Match", ascending=False, inplace=True) + if symbols["Match"].head().sum() < 300: + symbols["Match"] = symbols.apply( + lambda x: fuzz.partial_ratio(search, x["name"].lower()), + axis=1, + ) + + symbols.sort_values(by="Match", ascending=False, inplace=True) + symbols = symbols.head(10) + symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"]))) + self.searched_symbols[search] = symbol_list + return symbol_list + + def price_reply(self, symbol: Stock) -> str: + """Returns current market price or after hours if its available for a given stock symbol. + + Parameters + ---------- + symbols : list + List of stock symbols. + + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human readable + markdown formatted string of the symbols price and movement. + """ + + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol.id}/quote?token={self.IEX_TOKEN}" + + response = r.get(IEXurl) + if response.status_code == 200: + IEXData = response.json() + keys = ( + "isUSMarketOpen", + "extendedChangePercent", + "extendedPrice", + "companyName", + "latestPrice", + "changePercent", + ) + + if set(keys).issubset(IEXData): + + try: # Some symbols dont return if the market is open + IEXData["isUSMarketOpen"] + except KeyError: + IEXData["isUSMarketOpen"] = True + + if ( + IEXData["isUSMarketOpen"] + or (IEXData["extendedChangePercent"] is None) + or (IEXData["extendedPrice"] is None) + ): # Check if market is open. + message = f"The current stock price of {IEXData['companyName']} is $**{IEXData['latestPrice']}**" + change = round(IEXData["changePercent"] * 100, 2) + else: + message = ( + f"{IEXData['companyName']} closed at $**{IEXData['latestPrice']}**," + + f" after hours _(15 minutes delayed)_ the stock price is $**{IEXData['extendedPrice']}**" + ) + change = round(IEXData["extendedChangePercent"] * 100, 2) + + # Determine wording of change text + if change > 0: + message += f", the stock is currently **up {change}%**" + elif change < 0: + message += f", the stock is currently **down {change}%**" + else: + message += ", the stock hasn't shown any movement today." + else: + message = ( + f"The symbol: {symbol} encountered and error. This could be due to " + ) + + else: + message = f"The symbol: {symbol} was not found." + + return message + + def dividend_reply(self, symbol: Stock) -> str: + """Returns the most recent, or next dividend date for a stock symbol. + + Parameters + ---------- + symbols : list + List of stock symbols. + + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human readable formatted string of the symbols div dates. + """ + + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/dividends/next?token={self.IEX_TOKEN}" + response = r.get(IEXurl) + if response.status_code == 200 and response.json(): + IEXData = response.json()[0] + keys = ( + "amount", + "currency", + "declaredDate", + "exDate", + "frequency", + "paymentDate", + "flag", + ) + + if set(keys).issubset(IEXData): + + if IEXData["currency"] == "USD": + price = f"${IEXData['amount']}" + else: + price = f"{IEXData['amount']} {IEXData['currency']}" + + # Pattern IEX uses for dividend date. + pattern = "%Y-%m-%d" + + declared = datetime.strptime(IEXData["declaredDate"], pattern).strftime( + "%A, %B %w" + ) + ex = datetime.strptime(IEXData["exDate"], pattern).strftime("%A, %B %w") + payment = datetime.strptime(IEXData["paymentDate"], pattern).strftime( + "%A, %B %w" + ) + + daysDelta = ( + datetime.strptime(IEXData["paymentDate"], pattern) - datetime.now() + ).days + + return ( + "The next dividend for" + + f"${self.symbol_list[self.symbol_list['symbol']==symbol.id.upper()]['description'].item()}" + + f" is on {payment} which is in {daysDelta} days." + + f" The dividend is for {price} per share." + + f"\nThe dividend was declared on {declared} and the ex-dividend date is {ex}" + ) + + return f"{symbol} either doesn't exist or pays no dividend." + + def news_reply(self, symbol: Stock) -> str: + """Gets recent english news on stock symbols. + + Parameters + ---------- + symbols : list + List of stock symbols. + + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human readable markdown formatted string of the symbols news. + """ + + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/news/last/5?token={self.IEX_TOKEN}" + response = r.get(IEXurl) + if response.status_code == 200: + data = response.json() + if len(data): + message = f"News for **{symbol.id.upper()}**:\n\n" + for news in data: + if news["lang"] == "en" and not news["hasPaywall"]: + message = ( + f"*{news['source']}*: [{news['headline']}]({news['url']})\n" + ) + message += message + else: + return f"No news found for: {symbol}\nEither today is boring or the symbol does not exist." + else: + return f"No news found for: {symbol}\nEither today is boring or the symbol does not exist." + + return message + + def info_reply(self, symbol: Stock) -> str: + """Gets information on stock symbols. + + Parameters + ---------- + symbols : List[str] + List of stock symbols. + + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human readable formatted string of the symbols information. + """ + + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/company?token={self.IEX_TOKEN}" + response = r.get(IEXurl) + + if response.status_code == 200: + data = response.json() + message = ( + f"Company Name: [{data['companyName']}]({data['website']})\nIndustry:" + + f" {data['industry']}\nSector: {data['sector']}\nCEO: {data['CEO']}\nDescription: {data['description']}\n" + ) + + else: + message = f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." + + return message + + def intra_reply(self, symbol: Stock) -> pd.DataFrame: + """Returns price data for a symbol since the last market open. + + Parameters + ---------- + symbol : str + Stock symbol. + + Returns + ------- + pd.DataFrame + Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. + """ + if symbol.id.upper() not in list(self.symbol_list["symbol"]): + return pd.DataFrame() + + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/intraday-prices?token={self.IEX_TOKEN}" + response = r.get(IEXurl) + if response.status_code == 200: + df = pd.DataFrame(response.json()) + df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"]) + df["DT"] = pd.to_datetime(df["date"] + "T" + df["minute"]) + df = df.set_index("DT") + return df + + return pd.DataFrame() + + def chart_reply(self, symbol: Stock) -> pd.DataFrame: + """Returns price data for a symbol of the past month up until the previous trading days close. + Also caches multiple requests made in the same day. + + Parameters + ---------- + symbol : str + Stock symbol. + + Returns + ------- + pd.DataFrame + Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. + """ + schedule.run_pending() + + if symbol.id.upper() not in list(self.symbol_list["symbol"]): + return pd.DataFrame() + + try: # https://stackoverflow.com/a/3845776/8774114 + return self.charts[symbol.id.upper()] + except KeyError: + pass + + response = r.get( + f"https://cloud.iexapis.com/stable/stock/{symbol}/chart/1mm?token={self.IEX_TOKEN}&chartInterval=3&includeToday=false" + ) + + if response.status_code == 200: + df = pd.DataFrame(response.json()) + df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"]) + df["DT"] = pd.to_datetime(df["date"] + "T" + df["minute"]) + df = df.set_index("DT") + self.charts[symbol.id.upper()] = df + return df + + return pd.DataFrame() + + def stat_reply(self, symbol: Stock) -> str: + """Gets key statistics for each symbol in the list + + Parameters + ---------- + symbols : List[str] + List of stock symbols + + Returns + ------- + Dict[str, str] + Each symbol passed in is a key with its value being a human readable formatted string of the symbols statistics. + """ + + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/stats?token={self.IEX_TOKEN}" + response = r.get(IEXurl) + + if response.status_code == 200: + data = response.json() + [data.pop(k) for k in list(data) if data[k] == ""] + + m = "" + if "companyName" in data: + m += f"Company Name: {data['companyName']}\n" + if "marketcap" in data: + m += f"Market Cap: {data['marketcap']:,}\n" + if "week52high" in data: + m += f"52 Week (high-low): {data['week52high']:,} " + if "week52low" in data: + m += f"- {data['week52low']:,}\n" + if "employees" in data: + m += f"Number of Employees: {data['employees']:,}\n" + if "nextEarningsDate" in data: + m += f"Next Earnings Date: {data['nextEarningsDate']}\n" + if "peRatio" in data: + m += f"Price to Earnings: {data['peRatio']:.3f}\n" + if "beta" in data: + m += f"Beta: {data['beta']:.3f}\n" + return m + else: + return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." diff --git a/Symbol.py b/Symbol.py new file mode 100644 index 0000000..22bb11c --- /dev/null +++ b/Symbol.py @@ -0,0 +1,51 @@ +import requests as r + + +class Symbol: + """ + symbol: What the user calls it. ie tsla or btc + id: What the api expects. ie tsla or bitcoin + name: Human readable. ie Tesla or Bitcoin + """ + + currency = "usd" + pass + + def __init__(self, symbol) -> None: + self.symbol = symbol + self.id = symbol + self.name = symbol + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} instance of {self.id} at {id(self)}>" + + def __str__(self) -> str: + return self.id + + +class Stock(Symbol): + def __init__(self, symbol: str) -> None: + self.symbol = symbol + self.id = symbol + self.name = "$" + symbol.upper() + + +# Used by Coin to change symbols for ids +coins = r.get("https://api.coingecko.com/api/v3/coins/list").json() + + +class Coin(Symbol): + def __init__(self, symbol: str) -> None: + self.symbol = symbol + self.get_data() + + def get_data(self) -> None: + self.id = list(filter(lambda coin: coin["symbol"] == self.symbol, coins))[0][ + "id" + ] + data = r.get("https://api.coingecko.com/api/v3/coins/" + self.id).json() + self.data = data + + self.name = data["name"] + self.description = data["description"] + self.price = data["market_data"]["current_price"][self.currency] diff --git a/T_info.py b/T_info.py new file mode 100644 index 0000000..8a777d4 --- /dev/null +++ b/T_info.py @@ -0,0 +1,70 @@ +"""Functions and Info specific to the Telegram Bot +""" + +import re +import requests as r + + +class T_info: + license = re.sub( + r"\b\n", + " ", + r.get( + "https://gitlab.com/simple-stock-bots/simple-telegram-stock-bot/-/raw/master/LICENSE" + ).text, + ) + + help_text = """ +Thanks for using this bot, consider supporting it by [buying me a beer.](https://www.buymeacoffee.com/Anson) + +Keep up with the latest news for the bot in its Telegram Channel: https://t.me/simplestockbotnews + +Full documentation on using and running your own stock bot can be found [on the bots website.](https://simple-stock-bots.gitlab.io/site) + +The bot detects _"Symbols"_ using either one or two dollar signs before the symbol. One dollar sign is for a stock market ticker, while two is for a cryptocurrency coin. `/chart $$eth` would return a chart of the past month of data for Ethereum, while `/dividend $psec` returns dividend information for Prospect Capital stock. + +Simply calling a symbol in any message that the bot can see will also return the price. So a message like: `I wonder if $$btc will go to the Moon now that $tsla accepts it as payment` would return the current price for both Bitcoin and Tesla. + +**Commands** + - /donate [amount in USD] to donate. πŸŽ—οΈ + - /dividend $[symbol] will return dividend information for the symbol. πŸ“… + - /intra $[symbol] Plot of the stocks movement since the last market open. πŸ“ˆ + - /chart $[symbol] Plot of the stocks movement for the past 1 month. πŸ“Š + - /news $[symbol] News about the symbol. πŸ“° + - /info $[symbol] General information about the symbol. ℹ️ + - /stat $[symbol] Key statistics about the symbol. πŸ”’ + - /help Get some help using the bot. πŸ†˜ + +**Inline Features** + You can type @SimpleStockBot `[search]` in any chat or direct message to search for the stock bots full list of stock symbols and return the price of the ticker. Then once you select the ticker want the bot will send a message as you in that chat with the latest stock price. + + Market data is provided by [IEX Cloud](https://iexcloud.io) + + If you believe the bot is not behaving properly run `/status`. + """ + + donate_text = """ +Simple Stock Bot is run entirely on donations[.](https://www.buymeacoffee.com/Anson) +All donations go directly towards paying for servers, and market data is provided by +[IEX Cloud](https://iexcloud.io/). + +The easiest way to donate is to run the `/donate [amount in USD]` command with US dollars you would like to donate. + +Example: `/donate 2` would donate 2 USD. +An alternative way to donate is through https://www.buymeacoffee.com/Anson,which accepts Paypal or Credit card. +If you have any questions get in touch: @MisterBiggs or [anson@ansonbiggs.com](http://mailto:anson@ansonbiggs.com/) + +_Donations can only be made in a chat directly with @simplestockbot_ + """ + + +commands = """ +donate - Donate to the bot πŸŽ—οΈ +help - Get some help using the bot. πŸ†˜ +info - $[symbol] General information about the symbol. ℹ️ +news - $[symbol] News about the symbol. πŸ“° +stat - $[symbol] Key statistics about the symbol. πŸ”’ +dividend - $[symbol] Dividend info πŸ“… +intra - $[symbol] Plot since the last market open. πŸ“ˆ +chart - $[chart] Plot of the past month. πŸ“Š +""" # Not used by the bot but for updaing commands with BotFather diff --git a/bot.py b/bot.py index 2323975..db4c2d0 100644 --- a/bot.py +++ b/bot.py @@ -3,7 +3,6 @@ import datetime import io import logging import os -import random import html import json import traceback @@ -26,22 +25,20 @@ from telegram.ext import ( CallbackContext, ) -from functions import Symbol +from symbol_router import Router +from T_info import T_info TELEGRAM_TOKEN = os.environ["TELEGRAM"] -try: - IEX_TOKEN = os.environ["IEX"] -except KeyError: - IEX_TOKEN = "" - print("Starting without an IEX Token will not allow you to get market data!") try: STRIPE_TOKEN = os.environ["STRIPE"] except KeyError: STRIPE_TOKEN = "" print("Starting without a STRIPE Token will not allow you to accept Donations!") -s = Symbol(IEX_TOKEN) +s = Router() +t = T_info() + # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO @@ -53,42 +50,28 @@ print("Bot Online") def start(update: Update, context: CallbackContext): """Send a message when the command /start is issued.""" - update.message.reply_text(text=s.help_text, parse_mode=telegram.ParseMode.MARKDOWN) + update.message.reply_text(text=t.help_text, parse_mode=telegram.ParseMode.MARKDOWN) def help(update: Update, context: CallbackContext): """Send link to docs when the command /help is issued.""" - - update.message.reply_text(text=s.help_text, parse_mode=telegram.ParseMode.MARKDOWN) + update.message.reply_text(text=t.help_text, parse_mode=telegram.ParseMode.MARKDOWN) def license(update: Update, context: CallbackContext): """Return bots license agreement""" - - update.message.reply_text(text=s.license, parse_mode=telegram.ParseMode.MARKDOWN) + update.message.reply_text(text=t.license, parse_mode=telegram.ParseMode.MARKDOWN) def status(update: Update, context: CallbackContext): - message = "" - try: - # Bot Status - bot_resp = ( - datetime.datetime.now(update.message.date.tzinfo) - update.message.date - ) - message += f"It took {bot_resp.total_seconds()} seconds for the bot to get your message.\n" + bot_resp = datetime.datetime.now(update.message.date.tzinfo) - update.message.date - # IEX Status - message += s.iex_status() + "\n" - - # Message Status - message += s.message_status() - except Exception as ex: - message += ( - f"*\n\nERROR ENCOUNTERED:*\n{ex}\n\n" - + "*The bot encountered an error while attempting to find errors. Please contact the bot admin.*" - ) - - update.message.reply_text(text=message, parse_mode=telegram.ParseMode.MARKDOWN) + update.message.reply_text( + text=s.status( + f"It took {bot_resp.total_seconds()} seconds for the bot to get your message." + ), + parse_mode=telegram.ParseMode.MARKDOWN, + ) def donate(update: Update, context: CallbackContext): @@ -96,7 +79,7 @@ def donate(update: Update, context: CallbackContext): if update.message.text.strip() == "/donate": update.message.reply_text( - text=s.donate_text, parse_mode=telegram.ParseMode.MARKDOWN + text=t.donate_text, parse_mode=telegram.ParseMode.MARKDOWN ) return else: @@ -157,11 +140,10 @@ def symbol_detect(update: Update, context: CallbackContext): if symbols: # Let user know bot is working context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) - - for reply in s.price_reply(symbols).items(): - + print(symbols) + for reply in s.price_reply(symbols): update.message.reply_text( - text=reply[1], parse_mode=telegram.ParseMode.MARKDOWN + text=reply, parse_mode=telegram.ParseMode.MARKDOWN ) @@ -182,9 +164,9 @@ def dividend(update: Update, context: CallbackContext): if symbols: context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) - for symbol in symbols: + for reply in s.dividend_reply(symbols): update.message.reply_text( - text=s.dividend_reply(symbol), parse_mode=telegram.ParseMode.MARKDOWN + text=reply, parse_mode=telegram.ParseMode.MARKDOWN ) @@ -206,9 +188,9 @@ def news(update: Update, context: CallbackContext): if symbols: context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) - for reply in s.news_reply(symbols).items(): + for reply in s.news_reply(symbols): update.message.reply_text( - text=reply[1], parse_mode=telegram.ParseMode.MARKDOWN + text=reply, parse_mode=telegram.ParseMode.MARKDOWN ) @@ -230,9 +212,9 @@ def info(update: Update, context: CallbackContext): if symbols: context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) - for reply in s.info_reply(symbols).items(): + for reply in s.info_reply(symbols): update.message.reply_text( - text=reply[1], parse_mode=telegram.ParseMode.MARKDOWN + text=reply, parse_mode=telegram.ParseMode.MARKDOWN ) @@ -267,7 +249,14 @@ def intra(update: Update, context: CallbackContext): ) return - symbol = s.find_symbols(message)[0] + symbols = s.find_symbols(message) + symbol = symbols[0] + + if len(symbols): + symbol = symbols[0] + else: + update.message.reply_text("No symbols or coins found.") + return df = s.intra_reply(symbol) if df.empty: @@ -285,8 +274,8 @@ def intra(update: Update, context: CallbackContext): mpf.plot( df, type="renko", - title=f"\n${symbol.upper()}", - volume=True, + title=f"\n{symbol.name}", + volume="volume" in df.keys(), style="yahoo", mav=20, savefig=dict(fname=buf, dpi=400, bbox_inches="tight"), @@ -295,9 +284,9 @@ def intra(update: Update, context: CallbackContext): update.message.reply_photo( photo=buf, - caption=f"\nIntraday chart for ${symbol.upper()} from {df.first_valid_index().strftime('%I:%M')} to" + caption=f"\nIntraday chart for {symbol.name} from {df.first_valid_index().strftime('%I:%M')} to" + f" {df.last_valid_index().strftime('%I:%M')} ET on" - + f" {datetime.date.today().strftime('%d, %b %Y')}\n\n{s.price_reply([symbol])[symbol]}", + + f" {datetime.date.today().strftime('%d, %b %Y')}\n\n{s.price_reply([symbol])[0]}", parse_mode=telegram.ParseMode.MARKDOWN, ) @@ -314,7 +303,13 @@ def chart(update: Update, context: CallbackContext): ) return - symbol = s.find_symbols(message)[0] + symbols = s.find_symbols(message) + + if len(symbols): + symbol = symbols[0] + else: + update.message.reply_text("No symbols or coins found.") + return df = s.chart_reply(symbol) if df.empty: @@ -323,17 +318,16 @@ def chart(update: Update, context: CallbackContext): parse_mode=telegram.ParseMode.MARKDOWN, ) return - context.bot.send_chat_action( chat_id=chat_id, action=telegram.ChatAction.UPLOAD_PHOTO ) - + print(symbol) buf = io.BytesIO() mpf.plot( df, type="candle", - title=f"\n${symbol.upper()}", - volume=True, + title=f"\n{symbol.name}", + volume="volume" in df.keys(), style="yahoo", savefig=dict(fname=buf, dpi=400, bbox_inches="tight"), ) @@ -341,8 +335,8 @@ def chart(update: Update, context: CallbackContext): update.message.reply_photo( photo=buf, - caption=f"\n1 Month chart for ${symbol.upper()} from {df.first_valid_index().strftime('%d, %b %Y')}" - + f" to {df.last_valid_index().strftime('%d, %b %Y')}\n\n{s.price_reply([symbol])[symbol]}", + caption=f"\n1 Month chart for {symbol.name} from {df.first_valid_index().strftime('%d, %b %Y')}" + + f" to {df.last_valid_index().strftime('%d, %b %Y')}\n\n{s.price_reply([symbol])[0]}", parse_mode=telegram.ParseMode.MARKDOWN, ) @@ -365,38 +359,12 @@ def stat(update: Update, context: CallbackContext): if symbols: context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) - for reply in s.stat_reply(symbols).items(): + for reply in s.stat_reply(symbols): update.message.reply_text( - text=reply[1], parse_mode=telegram.ParseMode.MARKDOWN + text=reply, parse_mode=telegram.ParseMode.MARKDOWN ) -def crypto(update: Update, context: CallbackContext): - """ - https://iexcloud.io/docs/api/#cryptocurrency-quote - """ - context.bot.send_chat_action( - chat_id=update.message.chat_id, action=telegram.ChatAction.TYPING - ) - message = update.message.text - - if message.strip() == "/crypto": - update.message.reply_text( - "This command returns the current price in USD for a cryptocurrency.\nExample: /crypto eth" - ) - return - - reply = s.crypto_reply(message) - - if reply: - update.message.reply_text(text=reply, parse_mode=telegram.ParseMode.MARKDOWN) - else: - update.message.reply_text( - text=f"Pair: {message} returned an error.", - parse_mode=telegram.ParseMode.MARKDOWN, - ) - - def inline_query(update: Update, context: CallbackContext): """ Handles inline query. @@ -408,7 +376,7 @@ def inline_query(update: Update, context: CallbackContext): results = [] for match in matches: try: - price = s.price_reply([match[0]])[match[0]] + price = s.price_reply([match[0]])[0] results.append( InlineQueryResultArticle( match[0], @@ -429,13 +397,8 @@ def inline_query(update: Update, context: CallbackContext): def rand_pick(update: Update, context: CallbackContext): - choice = random.choice(list(s.symbol_list["description"])) - hold = ( - datetime.date.today() + datetime.timedelta(random.randint(1, 365)) - ).strftime("%b %d, %Y") - update.message.reply_text( - text=f"{choice}\nBuy and hold until: {hold}", + text=s.random_pick(), parse_mode=telegram.ParseMode.MARKDOWN, ) @@ -448,7 +411,18 @@ def error(update: Update, context: CallbackContext): None, context.error, context.error.__traceback__ ) tb_string = "".join(tb_list) + print(tb_string) + if update: + message = ( + f"An exception was raised while handling an update\n" + f"
update = {html.escape(json.dumps(update.to_dict(), indent=2, ensure_ascii=False))}"
+            "
\n\n" + f"
context.chat_data = {html.escape(str(context.chat_data))}
\n\n" + f"
context.user_data = {html.escape(str(context.user_data))}
\n\n" + f"
{html.escape(tb_string)}
" + ) +<<<<<<< HEAD message = ( f"An exception was raised while handling an update\n" f"
update = {html.escape(json.dumps(update.to_dict(), indent=2, ensure_ascii=False))}"
@@ -462,6 +436,11 @@ def error(update: Update, context: CallbackContext):
     print(message)
     # update.message.reply_text(text=message, parse_mode=telegram.ParseMode.HTML)
     # update.message.reply_text(text="Please inform the bot admin of this issue.")
+=======
+        # Finally, send the message
+        update.message.reply_text(text=message, parse_mode=telegram.ParseMode.HTML)
+        update.message.reply_text(text="Please inform the bot admin of this issue.")
+>>>>>>> crypto
 
 
 def main():
@@ -485,8 +464,7 @@ def main():
     dp.add_handler(CommandHandler("search", search))
     dp.add_handler(CommandHandler("intraday", intra))
     dp.add_handler(CommandHandler("intra", intra, run_async=True))
-    dp.add_handler(CommandHandler("chart", chart))
-    dp.add_handler(CommandHandler("crypto", crypto))
+    dp.add_handler(CommandHandler("chart", chart, run_async=True))
     dp.add_handler(CommandHandler("random", rand_pick))
     dp.add_handler(CommandHandler("donate", donate))
     dp.add_handler(CommandHandler("status", status))
@@ -511,9 +489,6 @@ def main():
     # Start the Bot
     updater.start_polling()
 
-    # Run the bot until you press Ctrl-C or the process receives SIGINT,
-    # SIGTERM or SIGABRT. This should be used most of the time, since
-    # start_polling() is non-blocking and will stop the bot gracefully.
     updater.idle()
 
 
diff --git a/cg_Crypto.py b/cg_Crypto.py
new file mode 100644
index 0000000..6323188
--- /dev/null
+++ b/cg_Crypto.py
@@ -0,0 +1,261 @@
+"""Class with functions for running the bot with IEX Cloud.
+"""
+
+from datetime import datetime
+from typing import Optional, List, Tuple
+
+import pandas as pd
+import requests as r
+import schedule
+from fuzzywuzzy import fuzz
+from markdownify import markdownify
+from Symbol import Coin
+
+
+class cg_Crypto:
+    """
+    Functions for finding crypto info
+    """
+
+    vs_currency = "usd"  # simple/supported_vs_currencies for list of options
+
+    searched_symbols = {}
+
+    def __init__(self) -> None:
+        """Creates a Symbol Object
+
+        Parameters
+        ----------
+        IEX_TOKEN : str
+            IEX Token
+        """
+        self.get_symbol_list()
+        schedule.every().day.do(self.get_symbol_list)
+
+    def symbol_id(self, symbol) -> str:
+        try:
+            return self.symbol_list[self.symbol_list["symbol"] == symbol]["id"].values[
+                0
+            ]
+        except KeyError:
+            return ""
+
+    def get_symbol_list(
+        self, return_df=False
+    ) -> Optional[Tuple[pd.DataFrame, datetime]]:
+
+        raw_symbols = r.get("https://api.coingecko.com/api/v3/coins/list").json()
+        symbols = pd.DataFrame(data=raw_symbols)
+
+        symbols["description"] = symbols["symbol"] + ": " + symbols["name"]
+        self.symbol_list = symbols
+        if return_df:
+            return symbols, datetime.now()
+
+    def status(self) -> str:
+        """Checks CoinGecko /ping endpoint for API issues.
+
+        Returns
+        -------
+        str
+            Human readable text on status of CoinGecko API
+        """
+        status = r.get("https://api.coingecko.com/api/v3/ping")
+
+        if status.status_code == 200:
+            return f"CoinGecko API responded that it was OK in {status.elapsed.total_seconds()} Seconds."
+        else:
+            return f"CoinGecko API returned an error in {status.elapsed.total_seconds()} Seconds."
+
+    def search_symbols(self, search: str) -> List[Tuple[str, str]]:
+        """Performs a fuzzy search to find coin symbols closest to a search term.
+
+        Parameters
+        ----------
+        search : str
+            String used to search, could be a company name or something close to the companies coin ticker.
+
+        Returns
+        -------
+        List[tuple[str, str]]
+            A list tuples of every coin sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name).
+        """
+        schedule.run_pending()
+        search = search.lower()
+        try:  # https://stackoverflow.com/a/3845776/8774114
+            return self.searched_symbols[search]
+        except KeyError:
+            pass
+
+        symbols = self.symbol_list
+        symbols["Match"] = symbols.apply(
+            lambda x: fuzz.ratio(search, f"{x['symbol']}".lower()),
+            axis=1,
+        )
+
+        symbols.sort_values(by="Match", ascending=False, inplace=True)
+        if symbols["Match"].head().sum() < 300:
+            symbols["Match"] = symbols.apply(
+                lambda x: fuzz.partial_ratio(search, x["name"].lower()),
+                axis=1,
+            )
+
+            symbols.sort_values(by="Match", ascending=False, inplace=True)
+        symbols = symbols.head(10)
+        symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"])))
+        self.searched_symbols[search] = symbol_list
+        return symbol_list
+
+    def price_reply(self, symbol: Coin) -> str:
+        """Returns current market price or after hours if its available for a given coin symbol.
+
+        Parameters
+        ----------
+        symbols : list
+            List of coin symbols.
+
+        Returns
+        -------
+        Dict[str, str]
+            Each symbol passed in is a key with its value being a human readable
+            markdown formatted string of the symbols price and movement.
+        """
+
+        response = r.get(
+            f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false"
+        )
+        if response.status_code == 200:
+            data = response.json()
+
+            try:
+                name = data["name"]
+                price = data["market_data"]["current_price"][self.vs_currency]
+                change = data["market_data"]["price_change_percentage_24h"]
+            except KeyError:
+                return f"{symbol} returned an error."
+
+            message = f"The current price of {name} is $**{price:,}**"
+
+            # Determine wording of change text
+            if change > 0:
+                message += f", the coin is currently **up {change:.3f}%** for today"
+            elif change < 0:
+                message += f", the coin is currently **down {change:.3f}%** for today"
+            else:
+                message += ", the coin hasn't shown any movement today."
+
+        else:
+            message = f"The Coin: {symbol.name} was not found."
+
+        return message
+
+    def intra_reply(self, symbol: Coin) -> pd.DataFrame:
+        """Returns price data for a symbol since the last market open.
+
+        Parameters
+        ----------
+        symbol : str
+            Stock symbol.
+
+        Returns
+        -------
+        pd.DataFrame
+            Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
+        """
+        response = r.get(
+            f"https://api.coingecko.com/api/v3/coins/{symbol.id}/ohlc?vs_currency=usd&days=1"
+        )
+        if response.status_code == 200:
+            df = pd.DataFrame(
+                response.json(), columns=["Date", "Open", "High", "Low", "Close"]
+            ).dropna()
+            df["Date"] = pd.to_datetime(df["Date"], unit="ms")
+            df = df.set_index("Date")
+            return df
+
+        return pd.DataFrame()
+
+    def chart_reply(self, symbol: Coin) -> pd.DataFrame:
+        """Returns price data for a symbol of the past month up until the previous trading days close.
+        Also caches multiple requests made in the same day.
+
+        Parameters
+        ----------
+        symbol : str
+            Stock symbol.
+
+        Returns
+        -------
+        pd.DataFrame
+            Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
+        """
+        response = r.get(
+            f"https://api.coingecko.com/api/v3/coins/{symbol.id}/ohlc?vs_currency=usd&days=30"
+        )
+
+        if response.status_code == 200:
+            df = pd.DataFrame(
+                response.json(), columns=["Date", "Open", "High", "Low", "Close"]
+            ).dropna()
+            df["Date"] = pd.to_datetime(df["Date"], unit="ms")
+            df = df.set_index("Date")
+            return df
+
+        return pd.DataFrame()
+
+    def stat_reply(self, symbol: Coin) -> str:
+        """Gets key statistics for each symbol in the list
+
+        Parameters
+        ----------
+        symbols : List[str]
+            List of coin symbols
+
+        Returns
+        -------
+        Dict[str, str]
+            Each symbol passed in is a key with its value being a human readable formatted string of the symbols statistics.
+        """
+        response = r.get(
+            f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false"
+        )
+        if response.status_code == 200:
+            data = response.json()
+
+            return f"""
+                [{data['name']}]({data['links']['homepage'][0]}) Statistics:
+                Maket Cap Ranking: {data.get('market_cap_rank',"Not Available")}
+                CoinGecko Scores:
+                    Overall: {data.get('coingecko_score','Not Available')}
+                    Development: {data.get('developer_score','Not Available')}
+                    Community: {data.get('community_score','Not Available')}
+                    Public Interest: {data.get('public_interest_score','Not Available')}
+                    """
+        else:
+            return f"{symbol.symbol} returned an error."
+
+    def info_reply(self, symbol: Coin) -> str:
+        """Gets information on stock symbols.
+
+        Parameters
+        ----------
+        symbols : List[str]
+            List of stock symbols.
+
+        Returns
+        -------
+        Dict[str, str]
+            Each symbol passed in is a key with its value being a human readable formatted string of the symbols information.
+        """
+
+        response = r.get(
+            f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false"
+        )
+        if response.status_code == 200:
+            data = response.json()
+            try:
+                return markdownify(data["description"]["en"])
+            except KeyError:
+                return f"{symbol} does not have a description available."
+
+        return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist."
diff --git a/commands b/commands
deleted file mode 100644
index d19d45d..0000000
--- a/commands
+++ /dev/null
@@ -1,8 +0,0 @@
-donate - Donate to the bot πŸŽ—οΈ
-help - Get some help using the bot. πŸ†˜
-info - $[symbol] General information about the symbol. ℹ️
-news - $[symbol] News about the symbol. πŸ“°
-stat - $[symbol] Key statistics about the symbol. πŸ”’
-dividend - $[symbol] Dividend info πŸ“…
-intra - $[symbol] Plot since the last market open.  πŸ“ˆ
-chart - $[chart] Plot of the past month. πŸ“Š
\ No newline at end of file
diff --git a/functions.py b/functions.py
deleted file mode 100644
index ca901b8..0000000
--- a/functions.py
+++ /dev/null
@@ -1,553 +0,0 @@
-import re
-from datetime import datetime
-from typing import Optional, List, Tuple, Dict
-
-import pandas as pd
-import requests as r
-import schedule
-from fuzzywuzzy import fuzz
-
-
-class Symbol:
-    """
-    Functions for finding stock market information about symbols.
-    """
-
-    SYMBOL_REGEX = "[$]([a-zA-Z]{1,4})"
-
-    searched_symbols = {}
-    charts = {}
-
-    license = re.sub(
-        r"\b\n",
-        " ",
-        r.get(
-            "https://gitlab.com/simple-stock-bots/simple-telegram-stock-bot/-/raw/master/LICENSE"
-        ).text,
-    )
-
-    help_text = """
-Thanks for using this bot, consider supporting it by [buying me a beer.](https://www.buymeacoffee.com/Anson)
-
-Keep up with the latest news for the bot in itsTelegram Channel: https://t.me/simplestockbotnews
-
-Full documentation on using and running your own stock bot can be found [here.](https://simple-stock-bots.gitlab.io/site)
-
-**Commands**
-        - /donate [amount in USD] to donate. πŸŽ—οΈ
-        - /dividend $[symbol] will return dividend information for the symbol. πŸ“…
-        - /intra $[symbol] Plot of the stocks movement since the last market open.  πŸ“ˆ
-        - /chart $[symbol] Plot of the stocks movement for the past 1 month. πŸ“Š
-        - /news $[symbol] News about the symbol. πŸ“°
-        - /info $[symbol] General information about the symbol. ℹ️
-        - /stat $[symbol] Key statistics about the symbol. πŸ”’
-        - /help Get some help using the bot. πŸ†˜
-
-**Inline Features**
-    You can type @SimpleStockBot `[search]` in any chat or direct message to search for the stock bots
-    full list of stock symbols and return the price of the ticker. Then once you select the ticker
-    want the bot will send a message as you in that chat with the latest stock price.
-    The bot also looks at every message in any chat it is in for stock symbols.Symbols start with a
-    `$` followed by the stock symbol. For example:$tsla would return price information for Tesla Motors.
-    Market data is provided by [IEX Cloud](https://iexcloud.io)
-
-    If you believe the bot is not behaving properly run `/status`.
-    """
-
-    donate_text = """
-Simple Stock Bot is run entirely on donations[.](https://www.buymeacoffee.com/Anson)
-All donations go directly towards paying for servers, and market data is provided by
-[IEX Cloud](https://iexcloud.io/).
-
-The easiest way to donate is to run the `/donate [amount in USD]` command with USdollars you would like to donate.
-
-Example: `/donate 2` would donate 2 USD.
-An alternative way to donate is through https://www.buymeacoffee.com/Anson,which accepts Paypal or Credit card.
-If you have any questions get in touch: @MisterBiggs or[anson@ansonbiggs.com](http://mailto:anson@ansonbiggs.com/)
-
-_Donations can only be made in a chat directly with @simplestockbot_
-    """
-
-    def __init__(self, IEX_TOKEN: str) -> None:
-        """Creates a Symbol Object
-
-        Parameters
-        ----------
-        IEX_TOKEN : str
-            IEX Token
-        """
-        self.IEX_TOKEN = IEX_TOKEN
-        if IEX_TOKEN != "":
-            self.get_symbol_list()
-
-            schedule.every().day.do(self.get_symbol_list)
-            schedule.every().day.do(self.clear_charts)
-
-    def clear_charts(self) -> None:
-        """Clears cache of chart data."""
-        self.charts = {}
-
-    def get_symbol_list(self, return_df=False) -> Optional[pd.DataFrame]:
-
-        raw_symbols = r.get(
-            f"https://cloud.iexapis.com/stable/ref-data/symbols?token={self.IEX_TOKEN}"
-        ).json()
-        symbols = pd.DataFrame(data=raw_symbols)
-
-        symbols["description"] = symbols["symbol"] + ": " + symbols["name"]
-        self.symbol_list = symbols
-        if return_df:
-            return symbols, datetime.now()
-
-    def iex_status(self) -> str:
-        """Checks IEX Status dashboard for any current API issues.
-
-        Returns
-        -------
-        str
-            Human readable text on status of IEX API
-        """
-        status = r.get("https://pjmps0c34hp7.statuspage.io/api/v2/status.json").json()[
-            "status"
-        ]
-
-        if status["indicator"] == "none":
-            return "IEX Cloud is currently not reporting any issues with its API."
-        else:
-            return (
-                f"{status['indicator']}: {status['description']}."
-                + " Please check the status page for more information. https://status.iexapis.com"
-            )
-
-    def message_status(self) -> str:
-        """Checks to see if the bot has available IEX Credits
-
-        Returns
-        -------
-        str
-            Human readable text on status of IEX Credits.
-        """
-        usage = r.get(
-            f"https://cloud.iexapis.com/stable/account/metadata?token={self.IEX_TOKEN}"
-        ).json()
-        try:
-            if (
-                usage["messagesUsed"] >= usage["messageLimit"] - 10000
-                and not usage["payAsYouGoEnabled"]
-            ):
-                return "Bot may be out of IEX Credits."
-            else:
-                return "Bot has available IEX Credits."
-        except KeyError:
-            return "**IEX API could not be reached.**"
-
-    def search_symbols(self, search: str) -> List[Tuple[str, str]]:
-        """Performs a fuzzy search to find stock symbols closest to a search term.
-
-        Parameters
-        ----------
-        search : str
-            String used to search, could be a company name or something close to the companies stock ticker.
-
-        Returns
-        -------
-        List[tuple[str, str]]
-            A list tuples of every stock sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name).
-        """
-
-        schedule.run_pending()
-        search = search.lower()
-        try:  # https://stackoverflow.com/a/3845776/8774114
-            return self.searched_symbols[search]
-        except KeyError:
-            pass
-
-        symbols = self.symbol_list
-        symbols["Match"] = symbols.apply(
-            lambda x: fuzz.ratio(search, f"{x['symbol']}".lower()),
-            axis=1,
-        )
-
-        symbols.sort_values(by="Match", ascending=False, inplace=True)
-        if symbols["Match"].head().sum() < 300:
-            symbols["Match"] = symbols.apply(
-                lambda x: fuzz.partial_ratio(search, x["name"].lower()),
-                axis=1,
-            )
-
-            symbols.sort_values(by="Match", ascending=False, inplace=True)
-        symbols = symbols.head(10)
-        symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"])))
-        self.searched_symbols[search] = symbol_list
-        return symbol_list
-
-    def find_symbols(self, text: str) -> List[str]:
-        """Finds stock tickers starting with a dollar sign in a blob of text and returns them in a list.
-        Only returns each match once. Example: Whats the price of $tsla?
-
-        Parameters
-        ----------
-        text : str
-            Blob of text.
-
-        Returns
-        -------
-        List[str]
-            List of stock symbols as strings without dollar sign.
-        """
-
-        return list(set(re.findall(self.SYMBOL_REGEX, text)))
-
-    def price_reply(self, symbols: list) -> Dict[str, str]:
-        """Returns current market price or after hours if its available for a given stock symbol.
-
-        Parameters
-        ----------
-        symbols : list
-            List of stock symbols.
-
-        Returns
-        -------
-        Dict[str, str]
-            Each symbol passed in is a key with its value being a human readable
-            markdown formatted string of the symbols price and movement.
-        """
-        dataMessages = {}
-        for symbol in symbols:
-            IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/quote?token={self.IEX_TOKEN}"
-
-            response = r.get(IEXurl)
-            if response.status_code == 200:
-                IEXData = response.json()
-                keys = (
-                    "isUSMarketOpen",
-                    "extendedChangePercent",
-                    "extendedPrice",
-                    "companyName",
-                    "latestPrice",
-                    "changePercent",
-                )
-                if set(keys).issubset(IEXData):
-
-                    try:  # Some symbols dont return if the market is open
-                        IEXData["isUSMarketOpen"]
-                    except KeyError:
-                        IEXData["isUSMarketOpen"] = True
-
-                    if (
-                        IEXData["isUSMarketOpen"]
-                        or (IEXData["extendedChangePercent"] is None)
-                        or (IEXData["extendedPrice"] is None)
-                    ):  # Check if market is open.
-
-                        message = f"The current stock price of {IEXData['companyName']} is $**{IEXData['latestPrice']}**"
-                        try:
-                            change = round(IEXData["changePercent"] * 100, 2)
-                        except (KeyError, TypeError):
-                            change = 0
-                    else:
-                        message = (
-                            f"{IEXData['companyName']} closed at $**{IEXData['latestPrice']}**,"
-                            + f" after hours _(15 minutes delayed)_ the stock price is $**{IEXData['extendedPrice']}**"
-                        )
-                        change = round(IEXData["extendedChangePercent"] * 100, 2)
-
-                    # Determine wording of change text
-                    if change > 0:
-                        message += f", the stock is currently **up {change}%**"
-                    elif change < 0:
-                        message += f", the stock is currently **down {change}%**"
-                    else:
-                        message += ", the stock hasn't shown any movement today."
-                else:
-                    message = f"The symbol: {symbol} encountered and error. This could be due to the symbol not being fully supported by IEX Cloud."
-
-            else:
-                message = f"The symbol: {symbol} was not found."
-
-            if symbol.upper() == "GME":
-                message += "\n\nπŸ™ŒπŸ’ŽPower to the PlayersπŸ’ŽπŸ™Œ"
-            if IEXData["latestPrice"] is None:
-                message = f"{symbol} has not reported price info to IEX Cloud."
-
-            dataMessages[symbol] = message
-        return dataMessages
-
-    def dividend_reply(self, symbol: str) -> Dict[str, str]:
-        """Returns the most recent, or next dividend date for a stock symbol.
-
-        Parameters
-        ----------
-        symbols : list
-            List of stock symbols.
-
-        Returns
-        -------
-        Dict[str, str]
-            Each symbol passed in is a key with its value being a human readable formatted string of the symbols div dates.
-        """
-
-        IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/dividends/next?token={self.IEX_TOKEN}"
-        response = r.get(IEXurl)
-        if response.status_code == 200 and response.json():
-            IEXData = response.json()[0]
-            keys = (
-                "amount",
-                "currency",
-                "declaredDate",
-                "exDate",
-                "frequency",
-                "paymentDate",
-                "flag",
-            )
-
-            if set(keys).issubset(IEXData):
-
-                if IEXData["currency"] == "USD":
-                    price = f"${IEXData['amount']}"
-                else:
-                    price = f"{IEXData['amount']} {IEXData['currency']}"
-
-                # Pattern IEX uses for dividend date.
-                pattern = "%Y-%m-%d"
-
-                declared = datetime.strptime(IEXData["declaredDate"], pattern).strftime(
-                    "%A, %B %w"
-                )
-                ex = datetime.strptime(IEXData["exDate"], pattern).strftime("%A, %B %w")
-                payment = datetime.strptime(IEXData["paymentDate"], pattern).strftime(
-                    "%A, %B %w"
-                )
-
-                daysDelta = (
-                    datetime.strptime(IEXData["paymentDate"], pattern) - datetime.now()
-                ).days
-
-                return (
-                    f"The next dividend for ${self.symbol_list[self.symbol_list['symbol']==symbol.upper()]['description'].item()}"
-                    + f" is on {payment} which is in {daysDelta} days."
-                    + f" The dividend is for {price} per share."
-                    + f"\nThe dividend was declared on {declared} and the ex-dividend date is {ex}"
-                )
-
-        return f"{symbol} either doesn't exist or pays no dividend."
-
-    def news_reply(self, symbols: list) -> Dict[str, str]:
-        """Gets recent english news on stock symbols.
-
-        Parameters
-        ----------
-        symbols : list
-            List of stock symbols.
-
-        Returns
-        -------
-        Dict[str, str]
-            Each symbol passed in is a key with its value being a human readable markdown formatted string of the symbols news.
-        """
-        newsMessages = {}
-
-        for symbol in symbols:
-            IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/news/last/5?token={self.IEX_TOKEN}"
-            response = r.get(IEXurl)
-            if response.status_code == 200:
-                data = response.json()
-                if len(data):
-                    newsMessages[symbol] = f"News for **{symbol.upper()}**:\n\n"
-                    for news in data:
-                        if news["lang"] == "en" and not news["hasPaywall"]:
-                            message = f"*{news['source']}*: [{news['headline']}]({news['url']})\n"
-                            newsMessages[symbol] = newsMessages[symbol] + message
-                else:
-                    newsMessages[
-                        symbol
-                    ] = f"No news found for: {symbol}\nEither today is boring or the symbol does not exist."
-            else:
-                newsMessages[
-                    symbol
-                ] = f"No news found for: {symbol}\nEither today is boring or the symbol does not exist."
-
-        return newsMessages
-
-    def info_reply(self, symbols: List[str]) -> Dict[str, str]:
-        """Gets information on stock symbols.
-
-        Parameters
-        ----------
-        symbols : List[str]
-            List of stock symbols.
-
-        Returns
-        -------
-        Dict[str, str]
-            Each symbol passed in is a key with its value being a human readable formatted string of the symbols information.
-        """
-        infoMessages = {}
-
-        for symbol in symbols:
-            IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/company?token={self.IEX_TOKEN}"
-            response = r.get(IEXurl)
-
-            if response.status_code == 200:
-                data = response.json()
-                infoMessages[symbol] = (
-                    f"Company Name: [{data['companyName']}]({data['website']})\nIndustry:"
-                    + f" {data['industry']}\nSector: {data['sector']}\nCEO: {data['CEO']}\nDescription: {data['description']}\n"
-                )
-
-            else:
-                infoMessages[
-                    symbol
-                ] = f"No information found for: {symbol}\nEither today is boring or the symbol does not exist."
-
-        return infoMessages
-
-    def intra_reply(self, symbol: str) -> pd.DataFrame:
-        """Returns price data for a symbol since the last market open.
-
-        Parameters
-        ----------
-        symbol : str
-            Stock symbol.
-
-        Returns
-        -------
-        pd.DataFrame
-            Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
-        """
-        if symbol.upper() not in list(self.symbol_list["symbol"]):
-            return pd.DataFrame()
-
-        IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/intraday-prices?token={self.IEX_TOKEN}"
-        response = r.get(IEXurl)
-        if response.status_code == 200:
-            df = pd.DataFrame(response.json())
-            df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"])
-            df["DT"] = pd.to_datetime(df["date"] + "T" + df["minute"])
-            df = df.set_index("DT")
-            return df
-
-        return pd.DataFrame()
-
-    def chart_reply(self, symbol: str) -> pd.DataFrame:
-        """Returns price data for a symbol of the past month up until the previous trading days close.
-        Also caches multiple requests made in the same day.
-
-        Parameters
-        ----------
-        symbol : str
-            Stock symbol.
-
-        Returns
-        -------
-        pd.DataFrame
-            Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
-        """
-        schedule.run_pending()
-
-        if symbol.upper() not in list(self.symbol_list["symbol"]):
-            return pd.DataFrame()
-
-        try:  # https://stackoverflow.com/a/3845776/8774114
-            return self.charts[symbol.upper()]
-        except KeyError:
-            pass
-
-        response = r.get(
-            f"https://cloud.iexapis.com/stable/stock/{symbol}/chart/1mm?token={self.IEX_TOKEN}&chartInterval=3&includeToday=false"
-        )
-
-        if response.status_code == 200:
-            df = pd.DataFrame(response.json())
-            df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"])
-            df["DT"] = pd.to_datetime(df["date"] + "T" + df["minute"])
-            df = df.set_index("DT")
-            self.charts[symbol.upper()] = df
-            return df
-
-        return pd.DataFrame()
-
-    def stat_reply(self, symbols: List[str]) -> Dict[str, str]:
-        """Gets key statistics for each symbol in the list
-
-        Parameters
-        ----------
-        symbols : List[str]
-            List of stock symbols
-
-        Returns
-        -------
-        Dict[str, str]
-            Each symbol passed in is a key with its value being a human readable formatted string of the symbols statistics.
-        """
-        infoMessages = {}
-
-        for symbol in symbols:
-            IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/stats?token={self.IEX_TOKEN}"
-            response = r.get(IEXurl)
-
-            if response.status_code == 200:
-                data = response.json()
-                [data.pop(k) for k in list(data) if data[k] == ""]
-
-                m = ""
-                if "companyName" in data:
-                    m += f"Company Name: {data['companyName']}\n"
-                if "marketcap" in data:
-                    m += f"Market Cap: {data['marketcap']:,}\n"
-                if "week52high" in data:
-                    m += f"52 Week (high-low): {data['week52high']:,} "
-                if "week52low" in data:
-                    m += f"- {data['week52low']:,}\n"
-                if "employees" in data:
-                    m += f"Number of Employees: {data['employees']:,}\n"
-                if "nextEarningsDate" in data:
-                    m += f"Next Earnings Date: {data['nextEarningsDate']}\n"
-                if "peRatio" in data:
-                    m += f"Price to Earnings: {data['peRatio']:.3f}\n"
-                if "beta" in data:
-                    m += f"Beta: {data['beta']:.3f}\n"
-                infoMessages[symbol] = m
-            else:
-                infoMessages[
-                    symbol
-                ] = f"No information found for: {symbol}\nEither today is boring or the symbol does not exist."
-
-        return infoMessages
-
-    def crypto_reply(self, pair: str) -> str:
-        """Returns the current price of a cryptocurrency
-
-        Parameters
-        ----------
-        pair : str
-            symbol for the cryptocurrency, sometimes with a price pair like ETHUSD
-
-        Returns
-        -------
-        str
-            Returns a human readable markdown description of the price, or an empty string if no price was found.
-        """
-
-        pair = pair.split(" ")[-1].replace("/", "").upper()
-        pair += "USD" if len(pair) == 3 else pair
-
-        IEXurl = f"https://cloud.iexapis.com/stable/crypto/{pair}/quote?token={self.IEX_TOKEN}"
-
-        response = r.get(IEXurl)
-
-        if response.status_code == 200:
-            data = response.json()
-
-            quote = f"Symbol: {data['symbol']}\n"
-            quote += f"Price: ${data['latestPrice']}\n"
-
-            new, old = data["latestPrice"], data["previousClose"]
-            if old is not None:
-                change = (float(new) - float(old)) / float(old)
-                quote += f"Change: {change}\n"
-
-            return quote
-
-        else:
-            return ""
diff --git a/requirements.txt b/requirements.txt
index 26c7372..74ecaef 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,4 +4,5 @@ pandas==1.2.1
 fuzzywuzzy==0.18.0
 python-Levenshtein==0.12.1
 schedule==1.0.0
-mplfinance==0.12.7a5
\ No newline at end of file
+mplfinance==0.12.7a5
+markdownify==0.6.5
\ No newline at end of file
diff --git a/symbol_router.py b/symbol_router.py
new file mode 100644
index 0000000..b29136b
--- /dev/null
+++ b/symbol_router.py
@@ -0,0 +1,273 @@
+"""Function that routes symbols to the correct API provider.
+"""
+
+import re
+import pandas as pd
+import random
+import datetime
+
+from typing import List, Tuple
+
+from IEX_Symbol import IEX_Symbol
+from cg_Crypto import cg_Crypto
+
+from Symbol import Symbol, Stock, Coin
+
+
+class Router:
+    STOCK_REGEX = "(?:^|[^\\$])\\$([a-zA-Z]{1,4})"
+    CRYPTO_REGEX = "[$]{2}([a-zA-Z]{1,9})"
+
+    def __init__(self):
+        self.stock = IEX_Symbol()
+        self.crypto = cg_Crypto()
+
+    def find_symbols(self, text: str) -> List[Symbol]:
+        """Finds stock tickers starting with a dollar sign, and cryptocurrencies with two dollar signs
+        in a blob of text and returns them in a list.
+        Only returns each match once. Example: Whats the price of $tsla?
+
+        Parameters
+        ----------
+        text : str
+            Blob of text.
+
+        Returns
+        -------
+        List[str]
+            List of stock symbols as strings without dollar sign.
+        """
+        symbols = []
+        stocks = set(re.findall(self.STOCK_REGEX, text))
+        for stock in stocks:
+            if stock.upper() in self.stock.symbol_list["symbol"].values:
+                symbols.append(Stock(stock))
+            else:
+                print(f"{stock} is not in list of stocks")
+
+        coins = set(re.findall(self.CRYPTO_REGEX, text))
+        for coin in coins:
+            if coin.lower() in self.crypto.symbol_list["symbol"].values:
+                symbols.append(Coin(coin.lower()))
+            else:
+                print(f"{coin} is not in list of coins")
+        print(symbols)
+        return symbols
+
+    def status(self, bot_resp) -> str:
+        """Checks for any issues with APIs.
+
+        Returns
+        -------
+        str
+            Human readable text on status of the bot and relevant APIs
+        """
+
+        return f"""
+        Bot Status:
+        {bot_resp}
+
+        Stock Market Data:
+        {self.stock.status()}
+
+        Cryptocurrency Data:
+        {self.crypto.status()}
+        """
+
+    def search_symbols(self, search: str) -> List[Tuple[str, str]]:
+        """Performs a fuzzy search to find stock symbols closest to a search term.
+
+        Parameters
+        ----------
+        search : str
+            String used to search, could be a company name or something close to the companies stock ticker.
+
+        Returns
+        -------
+        List[tuple[str, str]]
+            A list tuples of every stock sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name).
+        """
+        # TODO add support for crypto
+        return self.stock.search_symbols(search)
+
+    def price_reply(self, symbols: list) -> List[str]:
+        """Returns current market price or after hours if its available for a given stock symbol.
+
+        Parameters
+        ----------
+        symbols : list
+            List of stock symbols.
+
+        Returns
+        -------
+        Dict[str, str]
+            Each symbol passed in is a key with its value being a human readable
+            markdown formatted string of the symbols price and movement.
+        """
+        replies = []
+
+        for symbol in symbols:
+            if isinstance(symbol, Stock):
+                replies.append(self.stock.price_reply(symbol))
+            elif isinstance(symbol, Coin):
+                replies.append(self.crypto.price_reply(symbol))
+            else:
+                print(f"{symbol} is not a Stock or Coin")
+
+        return replies
+
+    def dividend_reply(self, symbols: list) -> List[str]:
+        """Returns the most recent, or next dividend date for a stock symbol.
+
+        Parameters
+        ----------
+        symbols : list
+            List of stock symbols.
+
+        Returns
+        -------
+        Dict[str, str]
+            Each symbol passed in is a key with its value being a human readable formatted string of the symbols div dates.
+        """
+        replies = []
+        for symbol in symbols:
+            if isinstance(symbol, Stock):
+                replies.append(self.stock.dividend_reply(symbol))
+            elif isinstance(symbol, Coin):
+                replies.append("Cryptocurrencies do no have Dividends.")
+            else:
+                print(f"{symbol} is not a Stock or Coin")
+
+        return replies
+
+    def news_reply(self, symbols: list) -> List[str]:
+        """Gets recent english news on stock symbols.
+
+        Parameters
+        ----------
+        symbols : list
+            List of stock symbols.
+
+        Returns
+        -------
+        Dict[str, str]
+            Each symbol passed in is a key with its value being a human readable markdown formatted string of the symbols news.
+        """
+        replies = []
+
+        for symbol in symbols:
+            if isinstance(symbol, Stock):
+                replies.append(self.stock.news_reply(symbol))
+            elif isinstance(symbol, Coin):
+                # replies.append(self.crypto.news_reply(symbol))
+                replies.append("News is not yet supported for cryptocurrencies.")
+            else:
+                print(f"{symbol} is not a Stock or Coin")
+
+        return replies
+
+    def info_reply(self, symbols: list) -> List[str]:
+        """Gets information on stock symbols.
+
+        Parameters
+        ----------
+        symbols : List[str]
+            List of stock symbols.
+
+        Returns
+        -------
+        Dict[str, str]
+            Each symbol passed in is a key with its value being a human readable formatted string of the symbols information.
+        """
+        replies = []
+
+        for symbol in symbols:
+            if isinstance(symbol, Stock):
+                replies.append(self.stock.info_reply(symbol))
+            elif isinstance(symbol, Coin):
+                replies.append(self.crypto.info_reply(symbol))
+            else:
+                print(f"{symbol} is not a Stock or Coin")
+
+        return replies
+
+    def intra_reply(self, symbol: Symbol) -> pd.DataFrame:
+        """Returns price data for a symbol since the last market open.
+
+        Parameters
+        ----------
+        symbol : str
+            Stock symbol.
+
+        Returns
+        -------
+        pd.DataFrame
+            Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
+        """
+
+        if isinstance(symbol, Stock):
+            return self.stock.intra_reply(symbol)
+        elif isinstance(symbol, Coin):
+            return self.crypto.intra_reply(symbol)
+        else:
+            print(f"{symbol} is not a Stock or Coin")
+            return pd.DataFrame()
+
+    def chart_reply(self, symbol: Symbol) -> pd.DataFrame:
+        """Returns price data for a symbol of the past month up until the previous trading days close.
+        Also caches multiple requests made in the same day.
+
+        Parameters
+        ----------
+        symbol : str
+            Stock symbol.
+
+        Returns
+        -------
+        pd.DataFrame
+            Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
+        """
+        if isinstance(symbol, Stock):
+            return self.stock.chart_reply(symbol)
+        elif isinstance(symbol, Coin):
+            return self.crypto.chart_reply(symbol)
+        else:
+            print(f"{symbol} is not a Stock or Coin")
+            return pd.DataFrame()
+
+    def stat_reply(self, symbols: List[Symbol]) -> List[str]:
+        """Gets key statistics for each symbol in the list
+
+        Parameters
+        ----------
+        symbols : List[str]
+            List of stock symbols
+
+        Returns
+        -------
+        Dict[str, str]
+            Each symbol passed in is a key with its value being a human readable formatted string of the symbols statistics.
+        """
+        replies = []
+
+        for symbol in symbols:
+            if isinstance(symbol, Stock):
+                replies.append(self.stock.stat_reply(symbol))
+            elif isinstance(symbol, Coin):
+                replies.append(self.crypto.stat_reply(symbol))
+            else:
+                print(f"{symbol} is not a Stock or Coin")
+
+        return replies
+
+    def random_pick(self) -> str:
+
+        choice = random.choice(
+            list(self.stock.symbol_list["description"])
+            + list(self.crypto.symbol_list["description"])
+        )
+        hold = (
+            datetime.date.today() + datetime.timedelta(random.randint(1, 365))
+        ).strftime("%b %d, %Y")
+
+        return f"{choice}\nBuy and hold until: {hold}"