From b10517e0253537166a765714cef0f67d3e4562ae Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Fri, 12 Feb 2021 23:21:56 -0700 Subject: [PATCH 01/15] new file structure --- functions.py => IEX_Symbol.py | 55 +--- T_info.py | 191 +++++++++++++ bot.py | 17 +- cg_Crypto.py | 499 ++++++++++++++++++++++++++++++++++ 4 files changed, 705 insertions(+), 57 deletions(-) rename functions.py => IEX_Symbol.py (87%) create mode 100644 T_info.py create mode 100644 cg_Crypto.py diff --git a/functions.py b/IEX_Symbol.py similarity index 87% rename from functions.py rename to IEX_Symbol.py index 44e3d04..c7b565f 100644 --- a/functions.py +++ b/IEX_Symbol.py @@ -1,3 +1,6 @@ +"""Class with functions for running the bot with IEX Cloud. +""" + import re from datetime import datetime from typing import Optional, List, Tuple, Dict @@ -8,7 +11,7 @@ import schedule from fuzzywuzzy import fuzz -class Symbol: +class IEX_Symbol: """ Functions for finding stock market information about symbols. """ @@ -18,56 +21,6 @@ class Symbol: 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 diff --git a/T_info.py b/T_info.py new file mode 100644 index 0000000..92849da --- /dev/null +++ b/T_info.py @@ -0,0 +1,191 @@ +"""Functions and Info specific to the Telegram Bot +""" + +import re +import requests as r +import pandas as pd + +from typing import List, Dict + + +class T_info: + STOCK_REGEX = "[$]([a-zA-Z]{1,4})" + CRYPTO_REGEX = "[$$]([a-zA-Z]{1,9})" + + def find_symbols(self, text: str) -> List[str, str]: + """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 = list(set(re.findall(self.SYMBOL_REGEX, text))) + crypto = list(set(re.findall(self.SYMBOL_REGEX, text))) + return + + 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 status(self) -> str: + """Checks IEX Status dashboard for any current API issues. + + Returns + ------- + str + Human readable text on status of IEX API + """ + + 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. + """ + + 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. + """ + + 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. + """ + + 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. + """ + + 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. + """ + + 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. + """ + + 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. + """ diff --git a/bot.py b/bot.py index baa3c22..24ca337 100644 --- a/bot.py +++ b/bot.py @@ -26,7 +26,9 @@ from telegram.ext import ( CallbackContext, ) -from functions import Symbol +from IEX_Symbol import IEX_Symbol +from cg_Crypto import cg_Crypto +from T_info import T_info TELEGRAM_TOKEN = os.environ["TELEGRAM"] @@ -41,7 +43,10 @@ except KeyError: STRIPE_TOKEN = "" print("Starting without a STRIPE Token will not allow you to accept Donations!") -s = Symbol(IEX_TOKEN) +s = IEX_Symbol(IEX_TOKEN) +c = cg_Crypto() +t = T_info() + # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO @@ -53,19 +58,19 @@ 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): @@ -96,7 +101,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: diff --git a/cg_Crypto.py b/cg_Crypto.py new file mode 100644 index 0000000..de878e9 --- /dev/null +++ b/cg_Crypto.py @@ -0,0 +1,499 @@ +"""Class with functions for running the bot with CoinGecko +""" + +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 cg_Crypto: + """ + Functions for finding crypto info + """ + + SYMBOL_REGEX = "[$]([a-zA-Z]{1,4})" + + searched_symbols = {} + charts = {} + + 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']}**" + 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." + + 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: + 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 "" From 96321d7c07dce27ac0093fa6a83f8a51c39f27af Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 17 Feb 2021 21:09:03 -0700 Subject: [PATCH 02/15] mostly implemented router and IEX --- IEX_Symbol.py | 214 ++++++++++++++++++----------------------------- bot.py | 6 +- symbol_router.py | 212 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 296 insertions(+), 136 deletions(-) create mode 100644 symbol_router.py diff --git a/IEX_Symbol.py b/IEX_Symbol.py index c7b565f..69cc42b 100644 --- a/IEX_Symbol.py +++ b/IEX_Symbol.py @@ -151,7 +151,7 @@ class IEX_Symbol: return list(set(re.findall(self.SYMBOL_REGEX, text))) - def price_reply(self, symbols: list) -> Dict[str, str]: + def price_reply(self, symbol: str) -> str: """Returns current market price or after hours if its available for a given stock symbol. Parameters @@ -165,61 +165,60 @@ class IEX_Symbol: 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", + 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']}**" + 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 " ) - if set(keys).issubset(IEXData): + else: + message = f"The symbol: {symbol} was not found." - try: # Some symbols dont return if the market is open - IEXData["isUSMarketOpen"] - except KeyError: - IEXData["isUSMarketOpen"] = True + return message - 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." - - dataMessages[symbol] = message - - return dataMessages - - def dividend_reply(self, symbol: str) -> Dict[str, str]: + def dividend_reply(self, symbol: str) -> str: """Returns the most recent, or next dividend date for a stock symbol. Parameters @@ -278,7 +277,7 @@ class IEX_Symbol: return f"{symbol} either doesn't exist or pays no dividend." - def news_reply(self, symbols: list) -> Dict[str, str]: + def news_reply(self, symbol: str) -> str: """Gets recent english news on stock symbols. Parameters @@ -291,31 +290,27 @@ class IEX_Symbol: 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." + 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.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: - newsMessages[ - symbol - ] = f"No news found for: {symbol}\nEither today is boring or the symbol does not exist." + 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 newsMessages + return message - def info_reply(self, symbols: List[str]) -> Dict[str, str]: + def info_reply(self, symbol: str) -> str: """Gets information on stock symbols. Parameters @@ -328,25 +323,21 @@ class IEX_Symbol: 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) + 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" - ) + 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: - infoMessages[ - symbol - ] = f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." + else: + message = f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." - return infoMessages + return message def intra_reply(self, symbol: str) -> pd.DataFrame: """Returns price data for a symbol since the last market open. @@ -413,7 +404,7 @@ class IEX_Symbol: return pd.DataFrame() - def stat_reply(self, symbols: List[str]) -> Dict[str, str]: + def stat_reply(self, symbol: str) -> str: """Gets key statistics for each symbol in the list Parameters @@ -453,47 +444,6 @@ class IEX_Symbol: m += f"Price to Earnings: {data['peRatio']:.3f}\n" if "beta" in data: m += f"Beta: {data['beta']:.3f}\n" - infoMessages[symbol] = m + return 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 "" + return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." diff --git a/bot.py b/bot.py index 24ca337..5473fa0 100644 --- a/bot.py +++ b/bot.py @@ -26,8 +26,7 @@ from telegram.ext import ( CallbackContext, ) -from IEX_Symbol import IEX_Symbol -from cg_Crypto import cg_Crypto +from symbol_router import Router from T_info import T_info TELEGRAM_TOKEN = os.environ["TELEGRAM"] @@ -43,8 +42,7 @@ except KeyError: STRIPE_TOKEN = "" print("Starting without a STRIPE Token will not allow you to accept Donations!") -s = IEX_Symbol(IEX_TOKEN) -c = cg_Crypto() +s = Router(IEX=IEX_TOKEN) t = T_info() # Enable logging diff --git a/symbol_router.py b/symbol_router.py new file mode 100644 index 0000000..3091cab --- /dev/null +++ b/symbol_router.py @@ -0,0 +1,212 @@ +"""Function that routes symbols to the correct API provider. +""" + +import re +import requests as r +import pandas as pd + +from typing import List, Dict + +from IEX_Symbol import IEX_Symbol +from cg_Crypto import cg_Crypto + + +class Router: + STOCK_REGEX = "[$]([a-zA-Z]{1,4})" + CRYPTO_REGEX = "[$$]([a-zA-Z]{1,9})" + + def __init__(self, IEX_TOKEN=""): + self.symbol = IEX_Symbol(IEX_TOKEN) + self.crypto = cg_Crypto() + + def find_symbols(self, text: str) -> Dict[str, str]: + """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 = {} + symbols["stocks"] = list(set(re.findall(self.SYMBOL_REGEX, text))) + symbols["crypto"] = list(set(re.findall(self.SYMBOL_REGEX, text))) + return symbols + + def status(self) -> str: + """Checks for any issues with APIs. + + Returns + ------- + str + Human readable text on status of IEX API + """ + + def price_reply(self, symbols: dict) -> 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 = [] + + if symbols["stocks"]: + for s in symbols["stocks"]: + replies.append(self.symbol.price_reply(s)) + + if symbols["crypto"]: + for s in symbols["crypto"]: + replies.append(self.crypto.price_reply(s)) + + return replies + + def dividend_reply(self, symbols: dict) -> 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. + """ + replies = [] + + if symbols["stocks"]: + for s in symbols["stocks"]: + replies.append(self.symbol.price_reply(s)) + + if symbols["crypto"]: + for s in symbols["crypto"]: + replies.append(self.crypto.price_reply(s)) + + def news_reply(self, symbols: dict) -> 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 = [] + + if symbols["stocks"]: + for s in symbols["stocks"]: + replies.append(self.symbol.price_reply(s)) + + if symbols["crypto"]: + for s in symbols["crypto"]: + replies.append(self.crypto.price_reply(s)) + + return replies + + def info_reply(self, symbols: dict) -> 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 = [] + + if symbols["stocks"]: + for s in symbols["stocks"]: + replies.append(self.symbol.price_reply(s)) + + if symbols["crypto"]: + for s in symbols["crypto"]: + replies.append(self.crypto.price_reply(s)) + + return replies + + def intra_reply(self, symbol: str, type: 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 type == "stocks": + return self.symbol.intra_reply(symbol) + elif type == "crypto": + return self.crypto.intra_reply(symbol) + else: + raise f"Unknown type: {type}" + + def chart_reply(self, symbol: str, type: 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. + """ + if type == "stocks": + return self.symbol.intra_reply(symbol) + elif type == "crypto": + return self.crypto.intra_reply(symbol) + else: + raise f"Unknown type: {type}" + + 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. + """ + replies = [] + + if symbols["stocks"]: + for s in symbols["stocks"]: + replies.append(self.symbol.price_reply(s)) + + if symbols["crypto"]: + for s in symbols["crypto"]: + replies.append(self.crypto.price_reply(s)) \ No newline at end of file From 487ab109334601367c1679b08a6a5bba42c8189c Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 25 Feb 2021 11:21:59 -0700 Subject: [PATCH 03/15] fix for symbols without a dividend --- IEX_Symbol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IEX_Symbol.py b/IEX_Symbol.py index 69cc42b..afc69c2 100644 --- a/IEX_Symbol.py +++ b/IEX_Symbol.py @@ -234,7 +234,7 @@ class IEX_Symbol: IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/dividends/next?token={self.IEX_TOKEN}" response = r.get(IEXurl) - if response.status_code == 200: + if response.status_code == 200 and response.json(): IEXData = response.json()[0] keys = ( "amount", From 6e37e541d69e07865dcc8525db008345ff647f53 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Thu, 25 Feb 2021 21:37:13 -0700 Subject: [PATCH 04/15] just need to smash bugs and the update is done --- IEX_Symbol.py | 72 +++----- T_info.py | 134 -------------- cg_Crypto.py | 443 +++++++++++------------------------------------ requirements.txt | 3 +- symbol_router.py | 49 ++++-- 5 files changed, 160 insertions(+), 541 deletions(-) diff --git a/IEX_Symbol.py b/IEX_Symbol.py index afc69c2..44e5eb0 100644 --- a/IEX_Symbol.py +++ b/IEX_Symbol.py @@ -1,9 +1,8 @@ """Class with functions for running the bot with IEX Cloud. """ -import re from datetime import datetime -from typing import Optional, List, Tuple, Dict +from typing import Optional, List, Tuple import pandas as pd import requests as r @@ -134,23 +133,6 @@ class IEX_Symbol: 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, symbol: str) -> str: """Returns current market price or after hours if its available for a given stock symbol. @@ -417,33 +399,31 @@ class IEX_Symbol: 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) + 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] == ""] + 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." + 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/T_info.py b/T_info.py index 92849da..3e61560 100644 --- a/T_info.py +++ b/T_info.py @@ -3,34 +3,9 @@ import re import requests as r -import pandas as pd - -from typing import List, Dict class T_info: - STOCK_REGEX = "[$]([a-zA-Z]{1,4})" - CRYPTO_REGEX = "[$$]([a-zA-Z]{1,9})" - - def find_symbols(self, text: str) -> List[str, str]: - """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 = list(set(re.findall(self.SYMBOL_REGEX, text))) - crypto = list(set(re.findall(self.SYMBOL_REGEX, text))) - return - license = re.sub( r"\b\n", " ", @@ -80,112 +55,3 @@ If you have any questions get in touch: @MisterBiggs or[anson@ansonbiggs.com](ht _Donations can only be made in a chat directly with @simplestockbot_ """ - - def status(self) -> str: - """Checks IEX Status dashboard for any current API issues. - - Returns - ------- - str - Human readable text on status of IEX API - """ - - 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. - """ - - 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. - """ - - 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. - """ - - 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. - """ - - 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. - """ - - 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. - """ - - 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. - """ diff --git a/cg_Crypto.py b/cg_Crypto.py index de878e9..32b746b 100644 --- a/cg_Crypto.py +++ b/cg_Crypto.py @@ -1,14 +1,14 @@ -"""Class with functions for running the bot with CoinGecko +"""Class with functions for running the bot with IEX Cloud. """ -import re from datetime import datetime -from typing import Optional, List, Tuple, Dict +from typing import Optional, List, Tuple import pandas as pd import requests as r import schedule from fuzzywuzzy import fuzz +from markdownify import markdownify class cg_Crypto: @@ -16,12 +16,11 @@ class cg_Crypto: Functions for finding crypto info """ - SYMBOL_REGEX = "[$]([a-zA-Z]{1,4})" + vs_currency = "usd" # simple/supported_vs_currencies for list of options searched_symbols = {} - charts = {} - def __init__(self, IEX_TOKEN: str) -> None: + def __init__(self) -> None: """Creates a Symbol Object Parameters @@ -29,22 +28,20 @@ class cg_Crypto: IEX_TOKEN : str IEX Token """ - self.IEX_TOKEN = IEX_TOKEN - if IEX_TOKEN != "": - self.get_symbol_list() + self.get_symbol_list() + schedule.every().day.do(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 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[pd.DataFrame]: - raw_symbols = r.get( - f"https://cloud.iexapis.com/stable/ref-data/symbols?token={self.IEX_TOKEN}" - ).json() + 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"] @@ -52,62 +49,34 @@ class cg_Crypto: if return_df: return symbols, datetime.now() - def iex_status(self) -> str: - """Checks IEX Status dashboard for any current API issues. + def cg_status(self) -> str: + """Checks CoinGecko /ping endpoint for API issues. Returns ------- str - Human readable text on status of IEX API + Human readable text on status of CoinGecko API """ - status = r.get("https://pjmps0c34hp7.statuspage.io/api/v2/status.json").json()[ - "status" - ] + status = r.get("https://api.coingecko.com/api/v3/ping") - if status["indicator"] == "none": - return "IEX Cloud is currently not reporting any issues with its API." + if status.status_code == 200: + return "CoinGecko API responded that it was OK in {status.elapsed.total_seconds()} Seconds." 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.**" + return "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 stock symbols closest to a search term. + """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 stock ticker. + 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 stock sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name). + 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 @@ -134,30 +103,13 @@ class cg_Crypto: 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. + def price_reply(self, symbol: str) -> str: + """Returns current market price or after hours if its available for a given coin symbol. Parameters ---------- symbols : list - List of stock symbols. + List of coin symbols. Returns ------- @@ -165,188 +117,34 @@ class cg_Crypto: 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']}**" - 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." - - 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) + response = r.get( + f"https://api.coingecko.com/api/v3/coins/{symbol}?localization=false" + ) if response.status_code == 200: - IEXData = response.json()[0] - keys = ( - "amount", - "currency", - "declaredDate", - "exDate", - "frequency", - "paymentDate", - "flag", - ) + data = response.json() - if set(keys).issubset(IEXData): + 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." - if IEXData["currency"] == "USD": - price = f"${IEXData['amount']}" - else: - price = f"{IEXData['amount']} {IEXData['currency']}" + message = f"The current coin price of {name} is $**{price}**" - # 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." + # Determine wording of change text + if change > 0: + message += f", the coin is currently **up {change}%**" + elif change < 0: + message += f", the coin is currently **down {change}%**" else: - newsMessages[ - symbol - ] = f"No news found for: {symbol}\nEither today is boring or the symbol does not exist." + message += ", the coin hasn't shown any movement today." - return newsMessages + else: + message = f"The symbol: {symbol} was not found." - 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 + return message def intra_reply(self, symbol: str) -> pd.DataFrame: """Returns price data for a symbol since the last market open. @@ -361,16 +159,15 @@ class cg_Crypto: 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) + response = r.get( + "https://api.coingecko.com/api/v3/coins/{symbol}/ohlc?vs_currency=usd&days=1" + ) 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") + 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() @@ -389,111 +186,71 @@ class cg_Crypto: 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" + "https://api.coingecko.com/api/v3/coins/{symbol}/ohlc?vs_currency=usd&days=30" ) - 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 + 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, symbols: List[str]) -> Dict[str, str]: + def stat_reply(self, symbol: str) -> str: """Gets key statistics for each symbol in the list Parameters ---------- symbols : List[str] - List of stock symbols + 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. """ - 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) - + response = r.get( + f"https://api.coingecko.com/api/v3/coins/{symbol}?localization=false" + ) if response.status_code == 200: data = response.json() - quote = f"Symbol: {data['symbol']}\n" - quote += f"Price: ${data['latestPrice']}\n" + message = 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')} + """ + return message - new, old = data["latestPrice"], data["previousClose"] - if old is not None: - change = (float(new) - float(old)) / float(old) - quote += f"Change: {change}\n" + def info_reply(self, symbol: str) -> str: + """Gets information on stock symbols. - return quote + Parameters + ---------- + symbols : List[str] + List of stock symbols. - else: - return "" + 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}?localization=false" + ) + if response.status_code == 200: + data = response.json() + try: + return markdownify(data["description"]) + 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/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 index 3091cab..7fb747f 100644 --- a/symbol_router.py +++ b/symbol_router.py @@ -1,8 +1,7 @@ -"""Function that routes symbols to the correct API provider. +"""Function that routes symbols to the correct API provider. """ import re -import requests as r import pandas as pd from typing import List, Dict @@ -12,10 +11,10 @@ from cg_Crypto import cg_Crypto class Router: - STOCK_REGEX = "[$]([a-zA-Z]{1,4})" - CRYPTO_REGEX = "[$$]([a-zA-Z]{1,9})" + STOCK_REGEX = "(?:^|[^\\$])\\$([a-zA-Z]{1,4})" + CRYPTO_REGEX = "[$]{2}([a-zA-Z]{1,9})" - def __init__(self, IEX_TOKEN=""): + def __init__(self, IEX_TOKEN): self.symbol = IEX_Symbol(IEX_TOKEN) self.crypto = cg_Crypto() @@ -35,8 +34,11 @@ class Router: List of stock symbols as strings without dollar sign. """ symbols = {} - symbols["stocks"] = list(set(re.findall(self.SYMBOL_REGEX, text))) - symbols["crypto"] = list(set(re.findall(self.SYMBOL_REGEX, text))) + symbols["stocks"] = list(set(re.findall(self.STOCK_REGEX, text))) + symbols["crypto"] = [ + self.crypto.symbol_id(c) for c in set(re.findall(self.CRYPTO_REGEX, text)) + ] + return symbols def status(self) -> str: @@ -48,6 +50,22 @@ class Router: Human readable text on status of IEX API """ + def search_symbols(self, search: str) -> List[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.symbol.find_symbols(str) + def price_reply(self, symbols: dict) -> List[str]: """Returns current market price or after hours if its available for a given stock symbol. @@ -94,8 +112,7 @@ class Router: replies.append(self.symbol.price_reply(s)) if symbols["crypto"]: - for s in symbols["crypto"]: - replies.append(self.crypto.price_reply(s)) + replies.append("Cryptocurrencies do no have Dividends.") def news_reply(self, symbols: dict) -> List[str]: """Gets recent english news on stock symbols. @@ -167,7 +184,7 @@ class Router: else: raise f"Unknown type: {type}" - def chart_reply(self, symbol: str, type: str) -> pd.DataFrame: + def chart_reply(self, symbols: 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. @@ -181,12 +198,10 @@ class Router: pd.DataFrame Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. """ - if type == "stocks": - return self.symbol.intra_reply(symbol) - elif type == "crypto": - return self.crypto.intra_reply(symbol) - else: - raise f"Unknown type: {type}" + if symbols["stocks"]: + return self.symbol.intra_reply(symbol := symbols["stocks"][0]), symbol + if symbols["crypto"]: + return self.symbol.intra_reply(symbol := symbols["crypto"][0]), symbol def stat_reply(self, symbols: List[str]) -> Dict[str, str]: """Gets key statistics for each symbol in the list @@ -209,4 +224,4 @@ class Router: if symbols["crypto"]: for s in symbols["crypto"]: - replies.append(self.crypto.price_reply(s)) \ No newline at end of file + replies.append(self.crypto.price_reply(s)) From e08f3c4275769bb26ae9e9b6aa027bf5b75be457 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Sat, 27 Feb 2021 17:14:55 -0700 Subject: [PATCH 05/15] so many changes --- IEX_Symbol.py | 16 ++++++--- Symbol.py | 27 +++++++++++++++ bot.py | 56 +++++++++++++++---------------- cg_Crypto.py | 3 +- symbol_router.py | 85 +++++++++++++++++++++++++++++++++++------------- 5 files changed, 130 insertions(+), 57 deletions(-) create mode 100644 Symbol.py diff --git a/IEX_Symbol.py b/IEX_Symbol.py index 44e5eb0..48917af 100644 --- a/IEX_Symbol.py +++ b/IEX_Symbol.py @@ -8,6 +8,7 @@ import pandas as pd import requests as r import schedule from fuzzywuzzy import fuzz +import os class IEX_Symbol: @@ -20,7 +21,7 @@ class IEX_Symbol: searched_symbols = {} charts = {} - def __init__(self, IEX_TOKEN: str) -> None: + def __init__(self) -> None: """Creates a Symbol Object Parameters @@ -28,8 +29,15 @@ class IEX_Symbol: IEX_TOKEN : str IEX Token """ - self.IEX_TOKEN = IEX_TOKEN - if 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) @@ -148,7 +156,7 @@ class IEX_Symbol: markdown formatted string of the symbols price and movement. """ - IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/quote?token={self.IEX_TOKEN}" + IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol.id}/quote?token={self.IEX_TOKEN}" response = r.get(IEXurl) if response.status_code == 200: diff --git a/Symbol.py b/Symbol.py new file mode 100644 index 0000000..8e1a0a0 --- /dev/null +++ b/Symbol.py @@ -0,0 +1,27 @@ +import requests as r + + +class Symbol: + currency = "usd" + pass + + +class Stock(Symbol): + def __init__(self, symbol) -> None: + self.symbol = symbol + + +class Coin(Symbol): + def __init__(self, symbol) -> None: + self.symbol = symbol + self.get_data() + + def get_data(self) -> None: + data = r.get("https://api.coingecko.com/api/v3/coins/" + self.symbol).json() + + self.id = data["id"] + self.name = data["name"] + self.description = data["description"] + self.price = data["market_data"]["current_price"][self.currency] + + self.data = data diff --git a/bot.py b/bot.py index 5473fa0..c69e734 100644 --- a/bot.py +++ b/bot.py @@ -31,18 +31,13 @@ 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 = Router(IEX=IEX_TOKEN) +s = Router() t = T_info() # Enable logging @@ -160,11 +155,11 @@ 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): + print(reply) update.message.reply_text( - text=reply[1], parse_mode=telegram.ParseMode.MARKDOWN + text=reply, parse_mode=telegram.ParseMode.MARKDOWN ) @@ -270,7 +265,7 @@ def intra(update: Update, context: CallbackContext): ) return - symbol = s.find_symbols(message)[0] + symbol = s.find_symbols(message) df = s.intra_reply(symbol) if df.empty: @@ -289,7 +284,7 @@ def intra(update: Update, context: CallbackContext): df, type="renko", title=f"\n${symbol.upper()}", - volume=True, + volume="volume" in df.keys(), style="yahoo", mav=20, savefig=dict(fname=buf, dpi=400, bbox_inches="tight"), @@ -317,9 +312,9 @@ def chart(update: Update, context: CallbackContext): ) return - symbol = s.find_symbols(message)[0] + symbols = s.find_symbols(message) - df = s.chart_reply(symbol) + df, symbol = s.chart_reply(symbols) if df.empty: update.message.reply_text( text="Invalid symbol please see `/help` for usage details.", @@ -330,13 +325,13 @@ def chart(update: Update, context: CallbackContext): 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, + volume="volume" in df.keys(), style="yahoo", savefig=dict(fname=buf, dpi=400, bbox_inches="tight"), ) @@ -345,7 +340,7 @@ 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]}", + + f" to {df.last_valid_index().strftime('%d, %b %Y')}\n\n{s.price_reply(symbols)[0]}", parse_mode=telegram.ParseMode.MARKDOWN, ) @@ -451,19 +446,22 @@ 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)}
" + ) - 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)}
" - ) - - # 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.") + # 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.") + print("-" * 50) + print(tb_string) def main(): diff --git a/cg_Crypto.py b/cg_Crypto.py index 32b746b..d005cce 100644 --- a/cg_Crypto.py +++ b/cg_Crypto.py @@ -119,7 +119,7 @@ class cg_Crypto: """ response = r.get( - f"https://api.coingecko.com/api/v3/coins/{symbol}?localization=false" + f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false" ) if response.status_code == 200: data = response.json() @@ -189,6 +189,7 @@ class cg_Crypto: response = r.get( "https://api.coingecko.com/api/v3/coins/{symbol}/ohlc?vs_currency=usd&days=30" ) + print(response.status_code) if response.status_code == 200: df = pd.DataFrame( response.json(), columns=["Date", "Open", "High", "Low", "Close"] diff --git a/symbol_router.py b/symbol_router.py index 7fb747f..382d40f 100644 --- a/symbol_router.py +++ b/symbol_router.py @@ -2,6 +2,7 @@ """ import re +import requests as r import pandas as pd from typing import List, Dict @@ -14,8 +15,8 @@ class Router: STOCK_REGEX = "(?:^|[^\\$])\\$([a-zA-Z]{1,4})" CRYPTO_REGEX = "[$]{2}([a-zA-Z]{1,9})" - def __init__(self, IEX_TOKEN): - self.symbol = IEX_Symbol(IEX_TOKEN) + def __init__(self): + self.stock = IEX_Symbol() self.crypto = cg_Crypto() def find_symbols(self, text: str) -> Dict[str, str]: @@ -33,11 +34,20 @@ class Router: List[str] List of stock symbols as strings without dollar sign. """ - symbols = {} - symbols["stocks"] = list(set(re.findall(self.STOCK_REGEX, text))) - symbols["crypto"] = [ - self.crypto.symbol_id(c) for c in set(re.findall(self.CRYPTO_REGEX, text)) - ] + 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") return symbols @@ -64,7 +74,7 @@ class Router: 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.symbol.find_symbols(str) + return self.stock.find_symbols(search) def price_reply(self, symbols: dict) -> List[str]: """Returns current market price or after hours if its available for a given stock symbol. @@ -82,14 +92,14 @@ class Router: """ replies = [] - if symbols["stocks"]: - for s in symbols["stocks"]: - replies.append(self.symbol.price_reply(s)) - - if symbols["crypto"]: - for s in symbols["crypto"]: - replies.append(self.crypto.price_reply(s)) - + 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") + print(replies) return replies def dividend_reply(self, symbols: dict) -> Dict[str, str]: @@ -109,7 +119,7 @@ class Router: if symbols["stocks"]: for s in symbols["stocks"]: - replies.append(self.symbol.price_reply(s)) + replies.append(self.stock.price_reply(s)) if symbols["crypto"]: replies.append("Cryptocurrencies do no have Dividends.") @@ -131,7 +141,7 @@ class Router: if symbols["stocks"]: for s in symbols["stocks"]: - replies.append(self.symbol.price_reply(s)) + replies.append(self.stock.price_reply(s)) if symbols["crypto"]: for s in symbols["crypto"]: @@ -156,7 +166,7 @@ class Router: if symbols["stocks"]: for s in symbols["stocks"]: - replies.append(self.symbol.price_reply(s)) + replies.append(self.stock.price_reply(s)) if symbols["crypto"]: for s in symbols["crypto"]: @@ -178,7 +188,7 @@ class Router: Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. """ if type == "stocks": - return self.symbol.intra_reply(symbol) + return self.stock.intra_reply(symbol) elif type == "crypto": return self.crypto.intra_reply(symbol) else: @@ -199,9 +209,9 @@ class Router: Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. """ if symbols["stocks"]: - return self.symbol.intra_reply(symbol := symbols["stocks"][0]), symbol + return self.stock.intra_reply(symbol := symbols["stocks"][0]), symbol if symbols["crypto"]: - return self.symbol.intra_reply(symbol := symbols["crypto"][0]), symbol + return self.stock.intra_reply(symbol := symbols["crypto"][0]), symbol def stat_reply(self, symbols: List[str]) -> Dict[str, str]: """Gets key statistics for each symbol in the list @@ -220,8 +230,37 @@ class Router: if symbols["stocks"]: for s in symbols["stocks"]: - replies.append(self.symbol.price_reply(s)) + replies.append(self.stock.price_reply(s)) if symbols["crypto"]: for s in symbols["crypto"]: replies.append(self.crypto.price_reply(s)) + + +class Symbol: + currency = "usd" + pass + + def __repr__(self) -> str: + return f"{self.__class__.__name__} instance of {self.id} at {id(self)}" + + +class Stock(Symbol): + def __init__(self, symbol) -> None: + self.symbol = symbol + self.id = symbol + + +class Coin(Symbol): + def __init__(self, symbol) -> None: + self.symbol = symbol + self.get_data() + + def get_data(self) -> None: + self.id = cg_Crypto().symbol_id(self.symbol) + 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] \ No newline at end of file From 19c2daa9f9abc6cfca562b33c2ff4f7bd423982d Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Sun, 28 Feb 2021 00:44:31 -0700 Subject: [PATCH 06/15] I hate Typing --- IEX_Symbol.py | 33 +++++++------ bot.py | 20 +------- cg_Crypto.py | 20 +++++--- symbol_router.py | 124 ++++++++++++++++++++++++++++------------------- 4 files changed, 107 insertions(+), 90 deletions(-) diff --git a/IEX_Symbol.py b/IEX_Symbol.py index 48917af..fe8502d 100644 --- a/IEX_Symbol.py +++ b/IEX_Symbol.py @@ -10,6 +10,8 @@ import schedule from fuzzywuzzy import fuzz import os +from symbol_router import Stock + class IEX_Symbol: """ @@ -47,7 +49,9 @@ class IEX_Symbol: """Clears cache of chart data.""" self.charts = {} - def get_symbol_list(self, return_df=False) -> Optional[pd.DataFrame]: + 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}" @@ -141,7 +145,7 @@ class IEX_Symbol: self.searched_symbols[search] = symbol_list return symbol_list - def price_reply(self, symbol: str) -> str: + def price_reply(self, symbol: Stock) -> str: """Returns current market price or after hours if its available for a given stock symbol. Parameters @@ -208,7 +212,7 @@ class IEX_Symbol: return message - def dividend_reply(self, symbol: str) -> str: + def dividend_reply(self, symbol: Stock) -> str: """Returns the most recent, or next dividend date for a stock symbol. Parameters @@ -259,7 +263,8 @@ class IEX_Symbol: ).days return ( - f"The next dividend for ${self.symbol_list[self.symbol_list['symbol']==symbol.upper()]['description'].item()}" + "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}" @@ -267,7 +272,7 @@ class IEX_Symbol: return f"{symbol} either doesn't exist or pays no dividend." - def news_reply(self, symbol: str) -> str: + def news_reply(self, symbol: Stock) -> str: """Gets recent english news on stock symbols. Parameters @@ -286,7 +291,7 @@ class IEX_Symbol: if response.status_code == 200: data = response.json() if len(data): - message = f"News for **{symbol.upper()}**:\n\n" + message = f"News for **{symbol.id.upper()}**:\n\n" for news in data: if news["lang"] == "en" and not news["hasPaywall"]: message = ( @@ -300,7 +305,7 @@ class IEX_Symbol: return message - def info_reply(self, symbol: str) -> str: + def info_reply(self, symbol: Stock) -> str: """Gets information on stock symbols. Parameters @@ -329,7 +334,7 @@ class IEX_Symbol: return message - def intra_reply(self, symbol: str) -> pd.DataFrame: + def intra_reply(self, symbol: Stock) -> pd.DataFrame: """Returns price data for a symbol since the last market open. Parameters @@ -342,7 +347,7 @@ class IEX_Symbol: 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"]): + 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}" @@ -356,7 +361,7 @@ class IEX_Symbol: return pd.DataFrame() - def chart_reply(self, symbol: str) -> 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. @@ -372,11 +377,11 @@ class IEX_Symbol: """ schedule.run_pending() - if symbol.upper() not in list(self.symbol_list["symbol"]): + 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.upper()] + return self.charts[symbol.id.upper()] except KeyError: pass @@ -389,12 +394,12 @@ class IEX_Symbol: 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 + self.charts[symbol.id.upper()] = df return df return pd.DataFrame() - def stat_reply(self, symbol: str) -> str: + def stat_reply(self, symbol: Stock) -> str: """Gets key statistics for each symbol in the list Parameters diff --git a/bot.py b/bot.py index c69e734..a53bf85 100644 --- a/bot.py +++ b/bot.py @@ -67,26 +67,8 @@ def license(update: Update, context: CallbackContext): 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" - # 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(), parse_mode=telegram.ParseMode.MARKDOWN) def donate(update: Update, context: CallbackContext): diff --git a/cg_Crypto.py b/cg_Crypto.py index d005cce..714b934 100644 --- a/cg_Crypto.py +++ b/cg_Crypto.py @@ -9,6 +9,7 @@ import requests as r import schedule from fuzzywuzzy import fuzz from markdownify import markdownify +from symbol_router import Coin class cg_Crypto: @@ -39,7 +40,9 @@ class cg_Crypto: except KeyError: return "" - def get_symbol_list(self, return_df=False) -> Optional[pd.DataFrame]: + 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) @@ -103,7 +106,7 @@ class cg_Crypto: self.searched_symbols[search] = symbol_list return symbol_list - def price_reply(self, symbol: str) -> str: + def price_reply(self, symbol: Coin) -> str: """Returns current market price or after hours if its available for a given coin symbol. Parameters @@ -146,7 +149,7 @@ class cg_Crypto: return message - def intra_reply(self, symbol: str) -> pd.DataFrame: + def intra_reply(self, symbol: Coin) -> pd.DataFrame: """Returns price data for a symbol since the last market open. Parameters @@ -172,7 +175,7 @@ class cg_Crypto: return pd.DataFrame() - def chart_reply(self, symbol: str) -> 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. @@ -200,7 +203,7 @@ class cg_Crypto: return pd.DataFrame() - def stat_reply(self, symbol: str) -> str: + def stat_reply(self, symbol: Coin) -> str: """Gets key statistics for each symbol in the list Parameters @@ -219,7 +222,7 @@ class cg_Crypto: if response.status_code == 200: data = response.json() - message = f""" + return f""" [{data['name']}]({data['links']['homepage'][0]}) Statistics: Maket Cap Ranking: {data.get('market_cap_rank',"Not Available")} CoinGecko Scores: @@ -228,9 +231,10 @@ class cg_Crypto: Community: {data.get('community_score','Not Available')} Public Interest: {data.get('public_interest_score','Not Available')} """ - return message + else: + return f"{symbol.symbol} returned an error." - def info_reply(self, symbol: str) -> str: + def info_reply(self, symbol: Coin) -> str: """Gets information on stock symbols. Parameters diff --git a/symbol_router.py b/symbol_router.py index 382d40f..16d8135 100644 --- a/symbol_router.py +++ b/symbol_router.py @@ -5,7 +5,7 @@ import re import requests as r import pandas as pd -from typing import List, Dict +from typing import List, Tuple, TypeVar from IEX_Symbol import IEX_Symbol from cg_Crypto import cg_Crypto @@ -19,7 +19,7 @@ class Router: self.stock = IEX_Symbol() self.crypto = cg_Crypto() - def find_symbols(self, text: str) -> Dict[str, str]: + def find_symbols(self, text: str) -> List[any]: """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? @@ -60,7 +60,7 @@ class Router: Human readable text on status of IEX API """ - def search_symbols(self, search: str) -> List[str]: + def search_symbols(self, search: str) -> List[Tuple[str, str]]: """Performs a fuzzy search to find stock symbols closest to a search term. Parameters @@ -74,9 +74,9 @@ class Router: 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.find_symbols(search) + return self.stock.search_symbols(search) - def price_reply(self, symbols: dict) -> List[str]: + def price_reply(self, symbols: list) -> List[str]: """Returns current market price or after hours if its available for a given stock symbol. Parameters @@ -99,10 +99,10 @@ class Router: replies.append(self.crypto.price_reply(symbol)) else: print(f"{symbol} is not a Stock or Coin") - print(replies) + return replies - def dividend_reply(self, symbols: dict) -> Dict[str, str]: + def dividend_reply(self, symbols: list) -> List[str]: """Returns the most recent, or next dividend date for a stock symbol. Parameters @@ -116,15 +116,17 @@ class Router: 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") - if symbols["stocks"]: - for s in symbols["stocks"]: - replies.append(self.stock.price_reply(s)) + return replies - if symbols["crypto"]: - replies.append("Cryptocurrencies do no have Dividends.") - - def news_reply(self, symbols: dict) -> List[str]: + def news_reply(self, symbols: list) -> List[str]: """Gets recent english news on stock symbols. Parameters @@ -139,17 +141,18 @@ class Router: """ replies = [] - if symbols["stocks"]: - for s in symbols["stocks"]: - replies.append(self.stock.price_reply(s)) - - if symbols["crypto"]: - for s in symbols["crypto"]: - replies.append(self.crypto.price_reply(s)) + 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: dict) -> List[str]: + def info_reply(self, symbols: list) -> List[str]: """Gets information on stock symbols. Parameters @@ -164,17 +167,17 @@ class Router: """ replies = [] - if symbols["stocks"]: - for s in symbols["stocks"]: - replies.append(self.stock.price_reply(s)) - - if symbols["crypto"]: - for s in symbols["crypto"]: - replies.append(self.crypto.price_reply(s)) + 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: str, type: str) -> pd.DataFrame: + def intra_reply(self, symbol: str) -> pd.DataFrame: """Returns price data for a symbol since the last market open. Parameters @@ -187,14 +190,16 @@ class Router: pd.DataFrame Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. """ - if type == "stocks": + + if isinstance(symbol, Stock): return self.stock.intra_reply(symbol) - elif type == "crypto": + elif isinstance(symbol, Coin): return self.crypto.intra_reply(symbol) else: - raise f"Unknown type: {type}" + print(f"{symbol} is not a Stock or Coin") + return pd.DataFrame() - def chart_reply(self, symbols: str) -> 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. @@ -208,12 +213,15 @@ class Router: pd.DataFrame Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. """ - if symbols["stocks"]: - return self.stock.intra_reply(symbol := symbols["stocks"][0]), symbol - if symbols["crypto"]: - return self.stock.intra_reply(symbol := symbols["crypto"][0]), symbol + 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[str]) -> Dict[str, str]: + def stat_reply(self, symbols: List[str]) -> List[str]: """Gets key statistics for each symbol in the list Parameters @@ -228,31 +236,49 @@ class Router: """ replies = [] - if symbols["stocks"]: - for s in symbols["stocks"]: - replies.append(self.stock.price_reply(s)) + 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") - if symbols["crypto"]: - for s in symbols["crypto"]: - replies.append(self.crypto.price_reply(s)) + return replies + + +Sym = TypeVar("Sym", Stock, Coin) 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 + def __repr__(self) -> str: - return f"{self.__class__.__name__} instance of {self.id} at {id(self)}" + 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) -> None: + def __init__(self, symbol: str) -> None: self.symbol = symbol self.id = symbol class Coin(Symbol): - def __init__(self, symbol) -> None: + def __init__(self, symbol: str) -> None: self.symbol = symbol self.get_data() @@ -263,4 +289,4 @@ class Coin(Symbol): self.name = data["name"] self.description = data["description"] - self.price = data["market_data"]["current_price"][self.currency] \ No newline at end of file + self.price = data["market_data"]["current_price"][self.currency] From f7dc433ec73cce901818f79d1e2392617eee6024 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Sun, 28 Feb 2021 13:54:46 -0700 Subject: [PATCH 07/15] I think i wrangled the types --- Symbol.py | 36 ++++++++++++++++----- bot.py | 82 +++++++++++++----------------------------------- symbol_router.py | 66 +++++++++++--------------------------- 3 files changed, 68 insertions(+), 116 deletions(-) diff --git a/Symbol.py b/Symbol.py index 8e1a0a0..03e4333 100644 --- a/Symbol.py +++ b/Symbol.py @@ -1,27 +1,49 @@ import requests as r +from cg_Crypto import cg_Crypto 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 - -class Stock(Symbol): 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 + + +# This is so every Coin instance doesnt have to download entire list of coin symbols and id's +cg = cg_Crypto() class Coin(Symbol): - def __init__(self, symbol) -> None: + def __init__(self, symbol: str) -> None: self.symbol = symbol self.get_data() def get_data(self) -> None: - data = r.get("https://api.coingecko.com/api/v3/coins/" + self.symbol).json() + self.id = cg.symbol_id(self.symbol) + data = r.get("https://api.coingecko.com/api/v3/coins/" + self.id).json() + self.data = data - self.id = data["id"] self.name = data["name"] self.description = data["description"] self.price = data["market_data"]["current_price"][self.currency] - - self.data = data diff --git a/bot.py b/bot.py index a53bf85..8b5a5fc 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 @@ -56,18 +55,15 @@ def start(update: Update, context: CallbackContext): def help(update: Update, context: CallbackContext): """Send link to docs when the command /help is issued.""" - 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=t.license, parse_mode=telegram.ParseMode.MARKDOWN) def status(update: Update, context: CallbackContext): - update.message.reply_text(text=s.status(), parse_mode=telegram.ParseMode.MARKDOWN) @@ -139,7 +135,6 @@ def symbol_detect(update: Update, context: CallbackContext): context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) print(symbols) for reply in s.price_reply(symbols): - print(reply) update.message.reply_text( text=reply, parse_mode=telegram.ParseMode.MARKDOWN ) @@ -162,9 +157,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 ) @@ -186,9 +181,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 ) @@ -210,9 +205,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 ) @@ -247,8 +242,8 @@ def intra(update: Update, context: CallbackContext): ) return - symbol = s.find_symbols(message) - + symbols = s.find_symbols(message) + symbol = symbols[0] df = s.intra_reply(symbol) if df.empty: update.message.reply_text( @@ -265,7 +260,7 @@ def intra(update: Update, context: CallbackContext): mpf.plot( df, type="renko", - title=f"\n${symbol.upper()}", + title=f"\n${symbol.name}", volume="volume" in df.keys(), style="yahoo", mav=20, @@ -275,9 +270,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, ) @@ -295,8 +290,8 @@ def chart(update: Update, context: CallbackContext): return symbols = s.find_symbols(message) - - df, symbol = s.chart_reply(symbols) + symbol = symbols[0] + df = s.chart_reply(symbol) if df.empty: update.message.reply_text( text="Invalid symbol please see `/help` for usage details.", @@ -312,7 +307,7 @@ def chart(update: Update, context: CallbackContext): mpf.plot( df, type="candle", - title=f"\n${symbol.upper()}", + title=f"\n${symbol.name}", volume="volume" in df.keys(), style="yahoo", savefig=dict(fname=buf, dpi=400, bbox_inches="tight"), @@ -321,8 +316,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(symbols)[0]}", + 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, ) @@ -345,38 +340,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. @@ -388,7 +357,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], @@ -409,13 +378,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, ) @@ -467,8 +431,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)) @@ -493,9 +456,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/symbol_router.py b/symbol_router.py index 16d8135..f673cca 100644 --- a/symbol_router.py +++ b/symbol_router.py @@ -2,14 +2,17 @@ """ import re -import requests as r import pandas as pd +import random +import datetime -from typing import List, Tuple, TypeVar +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})" @@ -19,7 +22,7 @@ class Router: self.stock = IEX_Symbol() self.crypto = cg_Crypto() - def find_symbols(self, text: str) -> List[any]: + 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? @@ -177,7 +180,7 @@ class Router: return replies - def intra_reply(self, symbol: str) -> pd.DataFrame: + def intra_reply(self, symbol: Symbol) -> pd.DataFrame: """Returns price data for a symbol since the last market open. Parameters @@ -199,7 +202,7 @@ class Router: print(f"{symbol} is not a Stock or Coin") return pd.DataFrame() - def chart_reply(self, symbol: str) -> 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. @@ -221,7 +224,7 @@ class Router: print(f"{symbol} is not a Stock or Coin") return pd.DataFrame() - def stat_reply(self, symbols: List[str]) -> List[str]: + def stat_reply(self, symbols: List[Symbol]) -> List[str]: """Gets key statistics for each symbol in the list Parameters @@ -246,47 +249,14 @@ class Router: return replies + def random_pick(self) -> str: -Sym = TypeVar("Sym", Stock, Coin) + 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") - -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 - - 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 - - -class Coin(Symbol): - def __init__(self, symbol: str) -> None: - self.symbol = symbol - self.get_data() - - def get_data(self) -> None: - self.id = cg_Crypto().symbol_id(self.symbol) - 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] + return f"{choice}\nBuy and hold until: {hold}" From e1ed5fbf5729d8810228feb211dbb4756e82bbb7 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Sat, 27 Mar 2021 17:17:16 -0700 Subject: [PATCH 08/15] update to python3.9 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 6ab7f2c25d912c36de44dbe4a35faddb01f62c75 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Sat, 27 Mar 2021 17:31:23 -0700 Subject: [PATCH 09/15] #31 just need to fix bugs from Main --- IEX_Symbol.py | 2 +- Symbol.py | 10 ++++++---- bot.py | 11 ++++------- cg_Crypto.py | 14 +++++++------- symbol_router.py | 2 +- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/IEX_Symbol.py b/IEX_Symbol.py index fe8502d..60d6e14 100644 --- a/IEX_Symbol.py +++ b/IEX_Symbol.py @@ -10,7 +10,7 @@ import schedule from fuzzywuzzy import fuzz import os -from symbol_router import Stock +from Symbol import Stock class IEX_Symbol: diff --git a/Symbol.py b/Symbol.py index 03e4333..22bb11c 100644 --- a/Symbol.py +++ b/Symbol.py @@ -1,5 +1,4 @@ import requests as r -from cg_Crypto import cg_Crypto class Symbol: @@ -28,10 +27,11 @@ class Stock(Symbol): def __init__(self, symbol: str) -> None: self.symbol = symbol self.id = symbol + self.name = "$" + symbol.upper() -# This is so every Coin instance doesnt have to download entire list of coin symbols and id's -cg = cg_Crypto() +# Used by Coin to change symbols for ids +coins = r.get("https://api.coingecko.com/api/v3/coins/list").json() class Coin(Symbol): @@ -40,7 +40,9 @@ class Coin(Symbol): self.get_data() def get_data(self) -> None: - self.id = cg.symbol_id(self.symbol) + 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 diff --git a/bot.py b/bot.py index 8b5a5fc..77e0f49 100644 --- a/bot.py +++ b/bot.py @@ -260,7 +260,7 @@ def intra(update: Update, context: CallbackContext): mpf.plot( df, type="renko", - title=f"\n${symbol.name}", + title=f"\n{symbol.name}", volume="volume" in df.keys(), style="yahoo", mav=20, @@ -270,7 +270,7 @@ def intra(update: Update, context: CallbackContext): update.message.reply_photo( photo=buf, - caption=f"\nIntraday chart for ${symbol.name} 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])[0]}", parse_mode=telegram.ParseMode.MARKDOWN, @@ -298,7 +298,6 @@ 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 ) @@ -307,7 +306,7 @@ def chart(update: Update, context: CallbackContext): mpf.plot( df, type="candle", - title=f"\n${symbol.name}", + title=f"\n{symbol.name}", volume="volume" in df.keys(), style="yahoo", savefig=dict(fname=buf, dpi=400, bbox_inches="tight"), @@ -316,7 +315,7 @@ def chart(update: Update, context: CallbackContext): update.message.reply_photo( photo=buf, - caption=f"\n1 Month chart for ${symbol.name} from {df.first_valid_index().strftime('%d, %b %Y')}" + 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, ) @@ -406,8 +405,6 @@ def error(update: Update, context: CallbackContext): # 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.") - print("-" * 50) - print(tb_string) def main(): diff --git a/cg_Crypto.py b/cg_Crypto.py index 714b934..9a1bc0d 100644 --- a/cg_Crypto.py +++ b/cg_Crypto.py @@ -9,7 +9,7 @@ import requests as r import schedule from fuzzywuzzy import fuzz from markdownify import markdownify -from symbol_router import Coin +from Symbol import Coin class cg_Crypto: @@ -163,7 +163,7 @@ class cg_Crypto: Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. """ response = r.get( - "https://api.coingecko.com/api/v3/coins/{symbol}/ohlc?vs_currency=usd&days=1" + f"https://api.coingecko.com/api/v3/coins/{symbol.id}/ohlc?vs_currency=usd&days=1" ) if response.status_code == 200: df = pd.DataFrame( @@ -190,9 +190,9 @@ class cg_Crypto: Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. """ response = r.get( - "https://api.coingecko.com/api/v3/coins/{symbol}/ohlc?vs_currency=usd&days=30" + f"https://api.coingecko.com/api/v3/coins/{symbol.id}/ohlc?vs_currency=usd&days=30" ) - print(response.status_code) + if response.status_code == 200: df = pd.DataFrame( response.json(), columns=["Date", "Open", "High", "Low", "Close"] @@ -217,7 +217,7 @@ class cg_Crypto: 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}?localization=false" + f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false" ) if response.status_code == 200: data = response.json() @@ -249,12 +249,12 @@ class cg_Crypto: """ response = r.get( - f"https://api.coingecko.com/api/v3/coins/{symbol}?localization=false" + 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"]) + return markdownify(data["description"]["en"]) except KeyError: return f"{symbol} does not have a description available." diff --git a/symbol_router.py b/symbol_router.py index f673cca..e7c5de6 100644 --- a/symbol_router.py +++ b/symbol_router.py @@ -51,7 +51,7 @@ class Router: symbols.append(Coin(coin.lower())) else: print(f"{coin} is not in list of coins") - + print(symbols) return symbols def status(self) -> str: From 367fab669f421fa186b1b408d3e3d5ff6f02bdc1 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Sat, 27 Mar 2021 18:24:33 -0700 Subject: [PATCH 10/15] Coin formatting for price reply --- cg_Crypto.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cg_Crypto.py b/cg_Crypto.py index 9a1bc0d..56c3f99 100644 --- a/cg_Crypto.py +++ b/cg_Crypto.py @@ -134,18 +134,18 @@ class cg_Crypto: except KeyError: return f"{symbol} returned an error." - message = f"The current coin price of {name} is $**{price}**" + message = f"The current coin price of {name} is $**{price:,}**" # Determine wording of change text if change > 0: - message += f", the coin is currently **up {change}%**" + message += f", the coin is currently **up {change:.3f}%** for today" elif change < 0: - message += f", the coin is currently **down {change}%**" + 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 symbol: {symbol} was not found." + message = f"The Coin: {symbol.name} was not found." return message From b2c325bddbf8205e8793063807df4fbda7c49c25 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Sat, 27 Mar 2021 18:26:26 -0700 Subject: [PATCH 11/15] grammar --- cg_Crypto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cg_Crypto.py b/cg_Crypto.py index 56c3f99..fa58662 100644 --- a/cg_Crypto.py +++ b/cg_Crypto.py @@ -134,7 +134,7 @@ class cg_Crypto: except KeyError: return f"{symbol} returned an error." - message = f"The current coin price of {name} is $**{price:,}**" + message = f"The current price of {name} is $**{price:,}**" # Determine wording of change text if change > 0: From 621de911cd0d8a9b8e90a741d0c1f02aea59affc Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Sat, 27 Mar 2021 18:40:02 -0700 Subject: [PATCH 12/15] close #48 --- bot.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 77e0f49..6dbd90f 100644 --- a/bot.py +++ b/bot.py @@ -244,6 +244,13 @@ def intra(update: Update, context: CallbackContext): 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: update.message.reply_text( @@ -290,7 +297,13 @@ def chart(update: Update, context: CallbackContext): return 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.chart_reply(symbol) if df.empty: update.message.reply_text( From f65704b0bebed436ae03a4021eda1bbca947bf86 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Sat, 27 Mar 2021 19:10:51 -0700 Subject: [PATCH 13/15] fixed status command --- IEX_Symbol.py | 33 +++++++-------------------------- bot.py | 9 ++++++++- cg_Crypto.py | 6 +++--- symbol_router.py | 15 +++++++++++++-- 4 files changed, 31 insertions(+), 32 deletions(-) diff --git a/IEX_Symbol.py b/IEX_Symbol.py index 60d6e14..b29c6f3 100644 --- a/IEX_Symbol.py +++ b/IEX_Symbol.py @@ -63,7 +63,7 @@ class IEX_Symbol: if return_df: return symbols, datetime.now() - def iex_status(self) -> str: + def status(self) -> str: """Checks IEX Status dashboard for any current API issues. Returns @@ -71,9 +71,12 @@ class IEX_Symbol: str Human readable text on status of IEX API """ - status = r.get("https://pjmps0c34hp7.statuspage.io/api/v2/status.json").json()[ - "status" - ] + 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." @@ -83,28 +86,6 @@ class IEX_Symbol: + " 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. diff --git a/bot.py b/bot.py index 6dbd90f..7b89a2a 100644 --- a/bot.py +++ b/bot.py @@ -64,7 +64,14 @@ def license(update: Update, context: CallbackContext): def status(update: Update, context: CallbackContext): - update.message.reply_text(text=s.status(), parse_mode=telegram.ParseMode.MARKDOWN) + bot_resp = datetime.datetime.now(update.message.date.tzinfo) - update.message.date + + 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): diff --git a/cg_Crypto.py b/cg_Crypto.py index fa58662..6323188 100644 --- a/cg_Crypto.py +++ b/cg_Crypto.py @@ -52,7 +52,7 @@ class cg_Crypto: if return_df: return symbols, datetime.now() - def cg_status(self) -> str: + def status(self) -> str: """Checks CoinGecko /ping endpoint for API issues. Returns @@ -63,9 +63,9 @@ class cg_Crypto: status = r.get("https://api.coingecko.com/api/v3/ping") if status.status_code == 200: - return "CoinGecko API responded that it was OK in {status.elapsed.total_seconds()} Seconds." + return f"CoinGecko API responded that it was OK in {status.elapsed.total_seconds()} Seconds." else: - return "CoinGecko API returned an error in {status.elapsed.total_seconds()} Seconds." + 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. diff --git a/symbol_router.py b/symbol_router.py index e7c5de6..b29136b 100644 --- a/symbol_router.py +++ b/symbol_router.py @@ -54,13 +54,24 @@ class Router: print(symbols) return symbols - def status(self) -> str: + def status(self, bot_resp) -> str: """Checks for any issues with APIs. Returns ------- str - Human readable text on status of IEX API + 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]]: From cb2f21409639bf90a08364af6ecdc84cdc88d0f7 Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Sun, 28 Mar 2021 01:05:26 -0700 Subject: [PATCH 14/15] updated help text --- T_info.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/T_info.py b/T_info.py index 3e61560..8d66335 100644 --- a/T_info.py +++ b/T_info.py @@ -17,9 +17,13 @@ class T_info: 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 +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 [here.](https://simple-stock-bots.gitlab.io/site) +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. đŸŽ—ī¸ @@ -32,11 +36,8 @@ Full documentation on using and running your own stock bot can be found [here.]( - /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. + 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`. @@ -47,11 +48,11 @@ Simple Stock Bot is run entirely on donations[.](https://www.buymeacoffee.com/An 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. +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/) +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_ """ From ff3083a8e59dc11c33b9c6dcc6ad77b1db87de3f Mon Sep 17 00:00:00 2001 From: Anson Biggs Date: Sun, 28 Mar 2021 01:07:10 -0700 Subject: [PATCH 15/15] added commands list to T_info --- T_info.py | 12 ++++++++++++ commands | 8 -------- 2 files changed, 12 insertions(+), 8 deletions(-) delete mode 100644 commands diff --git a/T_info.py b/T_info.py index 8d66335..8a777d4 100644 --- a/T_info.py +++ b/T_info.py @@ -56,3 +56,15 @@ If you have any questions get in touch: @MisterBiggs or [anson@ansonbiggs.com](h _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/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