From f213f1e7e73ba97971d551d2049dfbe52ea05e59 Mon Sep 17 00:00:00 2001 From: Anson Date: Fri, 10 Apr 2020 02:38:34 -0700 Subject: [PATCH 1/8] updated all packages to newest versions --- bot.py | 37 +++++++++++++++++-------------------- functions.py | 4 +--- requirements.txt | 6 +++--- 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/bot.py b/bot.py index 39f3f3d..8521b8e 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,4 @@ -# Works with Python 3.7 +# Works with Python 3.8 import logging import os @@ -15,7 +15,6 @@ from telegram.ext import ( from functions import Symbol TELEGRAM_TOKEN = os.environ["TELEGRAM"] - IEX_TOKEN = os.environ["IEX"] s = Symbol(IEX_TOKEN) @@ -28,20 +27,18 @@ logger = logging.getLogger(__name__) print("Bot Online") -# Define a few command handlers. These usually take the two arguments bot and -# update. Error handlers also receive the raised TelegramError object in error. -def start(bot, update): +def start(update, context): """Send a message when the command /start is issued.""" update.message.reply_text("I am started and ready to go!") -def help(bot, update): +def help(update, context): """Send link to docs when the command /help is issued.""" message = "[Please see the documentaion for Bot information](https://simple-stock-bots.gitlab.io/site/telegram/)" update.message.reply_text(text=message, parse_mode=telegram.ParseMode.MARKDOWN) -def symbol_detect(bot, update): +def symbol_detect(update, context): """ Runs on any message that doesn't have a command and searches for symbols, then returns the prices of any symbols found. """ @@ -51,7 +48,7 @@ def symbol_detect(bot, update): if symbols: # Let user know bot is working - bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) + context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) for reply in s.price_reply(symbols).items(): @@ -60,7 +57,7 @@ def symbol_detect(bot, update): ) -def dividend(bot, update): +def dividend(update, context): """ waits for /dividend or /div command and then finds dividend info on that symbol. """ @@ -69,7 +66,7 @@ def dividend(bot, update): symbols = s.find_symbols(message) if symbols: - bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) + context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) for reply in s.symbol_name(symbols).items(): @@ -78,7 +75,7 @@ def dividend(bot, update): ) -def news(bot, update): +def news(update, context): """ waits for /news command and then finds news info on that symbol. """ @@ -87,7 +84,7 @@ def news(bot, update): symbols = s.find_symbols(message) if symbols: - bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) + context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) for reply in s.news_reply(symbols).items(): @@ -96,7 +93,7 @@ def news(bot, update): ) -def info(bot, update): +def info(update, context): """ waits for /info command and then finds info on that symbol. """ @@ -105,7 +102,7 @@ def info(bot, update): symbols = s.find_symbols(message) if symbols: - bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) + context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) for reply in s.info_reply(symbols).items(): @@ -114,17 +111,17 @@ def info(bot, update): ) -def inline_query(bot, update): +def inline_query(update, context): """ Handles inline query. Does a fuzzy search on input and returns stocks that are close. """ + print(update.inline_query.query) matches = s.search_symbols(update.inline_query.query) results = [] for match in matches: try: price = s.price_reply([match[0]])[match[0]] - print(price) results.append( InlineQueryResultArticle( match[0], @@ -138,19 +135,19 @@ def inline_query(bot, update): pass if len(results) == 5: - bot.answerInlineQuery(update.inline_query.id, results) + update.inline_query.answer(results) return -def error(bot, update, error): +def error(update, context): """Log Errors caused by Updates.""" logger.warning('Update "%s" caused error "%s"', update, error) def main(): - """Start the bot.""" + """Start the context.bot.""" # Create the EventHandler and pass it your bot's token. - updater = Updater(TELEGRAM_TOKEN) + updater = Updater(TELEGRAM_TOKEN, use_context=True) # Get the dispatcher to register handlers dp = updater.dispatcher diff --git a/functions.py b/functions.py index 2652c8a..6f3169a 100644 --- a/functions.py +++ b/functions.py @@ -39,9 +39,7 @@ class Symbol: 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.lower(), f"{x['Symbol']} {x['Issue_Name']}".lower() - ), + lambda x: fuzz.partial_ratio(search.lower(), x["Issue_Name"].lower()), axis=1, ) symbols.sort_values(by="Match", ascending=False, inplace=True) diff --git a/requirements.txt b/requirements.txt index f3830cf..7082b06 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -python-telegram-bot==11.1.0 -requests==2.21.0 -pandas==0.25.3 +python-telegram-bot==12.5.1 +requests==2.23.0 +pandas==1.0.3 fuzzywuzzy==0.18.0 python-Levenshtein==0.12.0 \ No newline at end of file From 3c22fe4d5c4c6b5df462e963d35e5f054e67aec0 Mon Sep 17 00:00:00 2001 From: Anson Date: Fri, 10 Apr 2020 03:13:30 -0700 Subject: [PATCH 2/8] added documentation for more confusing functions --- bot.py | 7 +++---- functions.py | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/bot.py b/bot.py index 8521b8e..1b824b9 100644 --- a/bot.py +++ b/bot.py @@ -68,7 +68,7 @@ def dividend(update, context): if symbols: context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) - for reply in s.symbol_name(symbols).items(): + for reply in s.dividend_reply(symbols).items(): update.message.reply_text( text=reply[1], parse_mode=telegram.ParseMode.MARKDOWN @@ -87,7 +87,6 @@ def news(update, context): context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) for reply in s.news_reply(symbols).items(): - update.message.reply_text( text=reply[1], parse_mode=telegram.ParseMode.MARKDOWN ) @@ -105,7 +104,6 @@ def info(update, context): context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) for reply in s.info_reply(symbols).items(): - update.message.reply_text( text=reply[1], parse_mode=telegram.ParseMode.MARKDOWN ) @@ -113,7 +111,7 @@ def info(update, context): def inline_query(update, context): """ - Handles inline query. + Handles inline query. Does a fuzzy search on input and returns stocks that are close. """ print(update.inline_query.query) @@ -132,6 +130,7 @@ def inline_query(update, context): ) ) except TypeError: + logging.warning(str(match)) pass if len(results) == 5: diff --git a/functions.py b/functions.py index 6f3169a..08ec9fa 100644 --- a/functions.py +++ b/functions.py @@ -9,6 +9,10 @@ from fuzzywuzzy import fuzz class Symbol: + """ + Functions for finding stock market information about symbols. + """ + SYMBOL_REGEX = "[$]([a-zA-Z]{1,4})" LIST_URL = "http://oatsreportable.finra.org/OATSReportableSecurities-SOD.txt" @@ -17,6 +21,13 @@ class Symbol: self.symbol_list, self.symbol_ts = self.get_symbol_list() def get_symbol_list(self): + """ + Fetches a list of stock market symbols from FINRA + + Returns: + pd.DataFrame -- [DataFrame with columns: Symbol | Issue_Name | Primary_Listing_Mkt + datetime -- The time when the list of symbols was fetched. The Symbol list is updated every open and close of every trading day. + """ raw_symbols = r.get(self.LIST_URL).text symbols = pd.DataFrame( [line.split("|") for line in raw_symbols.split("\n")][:-1] @@ -28,6 +39,15 @@ class Symbol: return symbols, datetime.now() def search_symbols(self, search: str): + """ + Performs a fuzzy search to find stock symbols closest to a search term. + + Arguments: + search {str} -- String used to search, could be a company name or something close to the companies stock ticker. + + Returns: + List of Tuples -- A list tuples of every stock sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name). + """ if self.symbol_ts - datetime.now() > timedelta(hours=12): self.symbol_list, self.symbol_ts = self.get_symbol_list() @@ -48,14 +68,26 @@ class Symbol: def find_symbols(self, text: str): """ - Takes a blob of text and returns a list of symbols without any repeats. + 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? -> ['tsla'] + + Arguments: + text {str} -- Blob of text that might contain tickers with the format: $TICKER + + Returns: + list -- List of every found match without the dollar sign. """ return list(set(re.findall(self.SYMBOL_REGEX, text))) def price_reply(self, symbols: list): """ - Takes a list of symbols and returns a dictionary of strings with information about the symbol. + Takes a list of symbols and replies with Markdown formatted text about the symbols price change for the day. + + Arguments: + symbols {list} -- List of stock market symbols. + + Returns: + dict -- Dictionary with keys of symbols and values of markdown formatted text example: {'tsla': 'The current stock price of Tesla Motors is $**420$$, the stock price is currently **up 42%**} """ dataMessages = {} for symbol in symbols: @@ -80,7 +112,7 @@ class Symbol: return dataMessages - def symbol_name(self, symbols: list): + def dividend_reply(self, symbols: list): divMessages = {} for symbol in symbols: From 3b96fdf4ed668c3f843313b1d284837bef428634 Mon Sep 17 00:00:00 2001 From: Anson Date: Tue, 14 Apr 2020 19:32:18 -0700 Subject: [PATCH 3/8] markdown_v2 support removed --- functions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/functions.py b/functions.py index 08ec9fa..f83e53e 100644 --- a/functions.py +++ b/functions.py @@ -48,7 +48,7 @@ class Symbol: Returns: List of Tuples -- A list tuples of every stock sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name). """ - if self.symbol_ts - datetime.now() > timedelta(hours=12): + if self.symbol_ts - datetime.now() > timedelta(hours=3): self.symbol_list, self.symbol_ts = self.get_symbol_list() symbols = self.symbol_list @@ -62,8 +62,9 @@ class Symbol: lambda x: fuzz.partial_ratio(search.lower(), x["Issue_Name"].lower()), axis=1, ) - symbols.sort_values(by="Match", ascending=False, inplace=True) + symbols.sort_values(by="Match", ascending=False, inplace=True) + symbols = symbols.head(10) return list(zip(list(symbols["Symbol"]), list(symbols["Description"]))) def find_symbols(self, text: str): From 1a7cfb71b6a0b4be320dbf17120978b56a880c77 Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 15 Apr 2020 06:47:05 -0700 Subject: [PATCH 4/8] caching symbol search results --- functions.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/functions.py b/functions.py index f83e53e..f48718c 100644 --- a/functions.py +++ b/functions.py @@ -15,6 +15,7 @@ class Symbol: SYMBOL_REGEX = "[$]([a-zA-Z]{1,4})" LIST_URL = "http://oatsreportable.finra.org/OATSReportableSecurities-SOD.txt" + searched_symbols = {} def __init__(self, IEX_TOKEN: str): self.IEX_TOKEN = IEX_TOKEN @@ -48,6 +49,10 @@ class Symbol: Returns: List of Tuples -- A list tuples of every stock sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name). """ + try: # https://stackoverflow.com/a/3845776/8774114 + return self.searched_symbols[search] + except KeyError: + pass if self.symbol_ts - datetime.now() > timedelta(hours=3): self.symbol_list, self.symbol_ts = self.get_symbol_list() @@ -65,7 +70,9 @@ class Symbol: symbols.sort_values(by="Match", ascending=False, inplace=True) symbols = symbols.head(10) - return list(zip(list(symbols["Symbol"]), list(symbols["Description"]))) + 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): """ From 28a90b2472bb49a83a6422dede8e79f2c447949c Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 15 Apr 2020 07:32:36 -0700 Subject: [PATCH 5/8] using iex api for symbol list --- functions.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/functions.py b/functions.py index f48718c..f3765b3 100644 --- a/functions.py +++ b/functions.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta import pandas as pd import requests as r from fuzzywuzzy import fuzz +import schedule class Symbol: @@ -14,14 +15,15 @@ class Symbol: """ SYMBOL_REGEX = "[$]([a-zA-Z]{1,4})" - LIST_URL = "http://oatsreportable.finra.org/OATSReportableSecurities-SOD.txt" + searched_symbols = {} def __init__(self, IEX_TOKEN: str): self.IEX_TOKEN = IEX_TOKEN - self.symbol_list, self.symbol_ts = self.get_symbol_list() + self.get_symbol_list() + schedule.every().day.do(self.get_symbol_list) - def get_symbol_list(self): + def get_symbol_list(self, return_df=False): """ Fetches a list of stock market symbols from FINRA @@ -29,15 +31,15 @@ class Symbol: pd.DataFrame -- [DataFrame with columns: Symbol | Issue_Name | Primary_Listing_Mkt datetime -- The time when the list of symbols was fetched. The Symbol list is updated every open and close of every trading day. """ - raw_symbols = r.get(self.LIST_URL).text - symbols = pd.DataFrame( - [line.split("|") for line in raw_symbols.split("\n")][:-1] - ) - symbols.columns = symbols.iloc[0] - symbols = symbols.drop(symbols.index[0]) - symbols = symbols.drop(symbols.index[-1]) - symbols["Description"] = symbols["Symbol"] + ": " + symbols["Issue_Name"] - return symbols, datetime.now() + 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 search_symbols(self, search: str): """ @@ -49,28 +51,26 @@ class Symbol: Returns: List of Tuples -- A list tuples of every stock sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name). """ + schedule.run_pending() try: # https://stackoverflow.com/a/3845776/8774114 return self.searched_symbols[search] except KeyError: pass - if self.symbol_ts - datetime.now() > timedelta(hours=3): - self.symbol_list, self.symbol_ts = self.get_symbol_list() symbols = self.symbol_list symbols["Match"] = symbols.apply( - lambda x: fuzz.ratio(search.lower(), f"{x['Symbol']}".lower()), axis=1, + lambda x: fuzz.ratio(search.lower(), 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.lower(), x["Issue_Name"].lower()), - axis=1, + lambda x: fuzz.partial_ratio(search.lower(), 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"]))) + symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"]))) self.searched_symbols[search] = symbol_list return symbol_list From 9552ef169f1708f1b463962b9cc605a4e8db5df6 Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 15 Apr 2020 07:32:42 -0700 Subject: [PATCH 6/8] formatting --- bot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 1b824b9..4406f9b 100644 --- a/bot.py +++ b/bot.py @@ -114,8 +114,9 @@ def inline_query(update, context): Handles inline query. Does a fuzzy search on input and returns stocks that are close. """ - print(update.inline_query.query) + matches = s.search_symbols(update.inline_query.query) + results = [] for match in matches: try: From 5fea3efee50086990f9f99cdd69c976351c6cfc9 Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 15 Apr 2020 07:38:49 -0700 Subject: [PATCH 7/8] prevent duplicate keys due to caps --- functions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/functions.py b/functions.py index f3765b3..a6967be 100644 --- a/functions.py +++ b/functions.py @@ -52,6 +52,7 @@ class Symbol: List of Tuples -- 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: @@ -59,13 +60,13 @@ class Symbol: symbols = self.symbol_list symbols["Match"] = symbols.apply( - lambda x: fuzz.ratio(search.lower(), f"{x['symbol']}".lower()), axis=1, + 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.lower(), x["name"].lower()), axis=1, + lambda x: fuzz.partial_ratio(search, x["name"].lower()), axis=1, ) symbols.sort_values(by="Match", ascending=False, inplace=True) From c994afddf60fc5e97b49f3979be48761c33a5603 Mon Sep 17 00:00:00 2001 From: Anson Date: Wed, 15 Apr 2020 07:43:17 -0700 Subject: [PATCH 8/8] added schedule --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7082b06..82c907b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ python-telegram-bot==12.5.1 requests==2.23.0 pandas==1.0.3 fuzzywuzzy==0.18.0 -python-Levenshtein==0.12.0 \ No newline at end of file +python-Levenshtein==0.12.0 +schedule==0.6.0 \ No newline at end of file