1
0
mirror of https://gitlab.com/simple-stock-bots/simple-stock-bot.git synced 2025-06-16 07:16:40 +00:00

Update symbol_router.py

This commit is contained in:
Anson Biggs 2021-10-06 14:08:11 +00:00
parent a9eaf7c6db
commit 0577018284

View File

@ -1,498 +1,498 @@
"""Function that routes symbols to the correct API provider. """Function that routes symbols to the correct API provider.
""" """
import datetime import datetime
import random import random
import re import re
from logging import critical, debug, error, info, warning from logging import critical, debug, error, info, warning
import pandas as pd import pandas as pd
import schedule import schedule
from cachetools import TTLCache, cached from cachetools import TTLCache, cached
from fuzzywuzzy import fuzz from fuzzywuzzy import fuzz
from cg_Crypto import cg_Crypto from cg_Crypto import cg_Crypto
from IEX_Symbol import IEX_Symbol from IEX_Symbol import IEX_Symbol
from Symbol import Coin, Stock, Symbol from Symbol import Coin, Stock, Symbol
class Router: class Router:
STOCK_REGEX = "(?:^|[^\\$])\\$([a-zA-Z.]{1,6})" STOCK_REGEX = "(?:^|[^\\$])\\$([a-zA-Z.]{1,6})"
CRYPTO_REGEX = "[$]{2}([a-zA-Z]{1,20})" CRYPTO_REGEX = "[$]{2}([a-zA-Z]{1,20})"
searched_symbols = {} searched_symbols = {}
trending_count = {} trending_count = {}
def __init__(self): def __init__(self):
self.stock = IEX_Symbol() self.stock = IEX_Symbol()
self.crypto = cg_Crypto() self.crypto = cg_Crypto()
schedule.every().hour.do(self.trending_decay) schedule.every().hour.do(self.trending_decay)
def trending_decay(self, decay=0.5): def trending_decay(self, decay=0.5):
"""Decays the value of each trending stock by a multiplier""" """Decays the value of each trending stock by a multiplier"""
t_copy = {}
if self.trending_count: if self.trending_count:
t_copy = self.trending_count.copy() t_copy = self.trending_count.copy()
for key in t_copy.keys(): for key in t_copy.keys():
if t_copy[key] < 0.01: if t_copy[key] < 0.01:
# This just makes sure were not keeping around keys that havent been called in a very long time. # This just makes sure were not keeping around keys that havent been called in a very long time.
t_copy.pop(key, None) t_copy.pop(key, None)
else: else:
t_copy[key] = t_copy[key] * decay t_copy[key] = t_copy[key] * decay
self.trending_count = t_copy.copy() self.trending_count = t_copy.copy()
info("Decayed trending symbols.") info("Decayed trending symbols.")
def find_symbols(self, text: str) -> list[Symbol]: def find_symbols(self, text: str) -> list[Symbol]:
"""Finds stock tickers starting with a dollar sign, and cryptocurrencies with two dollar signs """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. in a blob of text and returns them in a list.
Parameters Parameters
---------- ----------
text : str text : str
Blob of text. Blob of text.
Returns Returns
------- -------
list[Symbol] list[Symbol]
List of stock symbols as Symbol objects List of stock symbols as Symbol objects
""" """
schedule.run_pending() schedule.run_pending()
symbols = [] symbols = []
stocks = set(re.findall(self.STOCK_REGEX, text)) stocks = set(re.findall(self.STOCK_REGEX, text))
for stock in stocks: for stock in stocks:
if stock.upper() in self.stock.symbol_list["symbol"].values: if stock.upper() in self.stock.symbol_list["symbol"].values:
symbols.append(Stock(stock)) symbols.append(Stock(stock))
else: else:
info(f"{stock} is not in list of stocks") info(f"{stock} is not in list of stocks")
coins = set(re.findall(self.CRYPTO_REGEX, text)) coins = set(re.findall(self.CRYPTO_REGEX, text))
for coin in coins: for coin in coins:
if coin.lower() in self.crypto.symbol_list["symbol"].values: if coin.lower() in self.crypto.symbol_list["symbol"].values:
symbols.append(Coin(coin.lower())) symbols.append(Coin(coin.lower()))
else: else:
info(f"{coin} is not in list of coins") info(f"{coin} is not in list of coins")
if symbols: if symbols:
info(symbols) info(symbols)
for symbol in symbols: for symbol in symbols:
self.trending_count[symbol.tag] = ( self.trending_count[symbol.tag] = (
self.trending_count.get(symbol.tag, 0) + 1 self.trending_count.get(symbol.tag, 0) + 1
) )
return symbols return symbols
def status(self, bot_resp) -> str: def status(self, bot_resp) -> str:
"""Checks for any issues with APIs. """Checks for any issues with APIs.
Returns Returns
------- -------
str str
Human readable text on status of the bot and relevant APIs Human readable text on status of the bot and relevant APIs
""" """
stats = f""" stats = f"""
Bot Status: Bot Status:
{bot_resp} {bot_resp}
Stock Market Data: Stock Market Data:
{self.stock.status()} {self.stock.status()}
Cryptocurrency Data: Cryptocurrency Data:
{self.crypto.status()} {self.crypto.status()}
""" """
warning(stats) warning(stats)
return stats return stats
def search_symbols(self, search: str) -> list[tuple[str, str]]: 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 stock symbols closest to a search term.
Parameters Parameters
---------- ----------
search : str 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 stock ticker.
Returns Returns
------- -------
list[tuple[str, str]] list[tuple[str, str]]
A list tuples of every stock sorted in order of how well they match. A list tuples of every stock sorted in order of how well they match.
Each tuple contains: (Symbol, Issue Name). Each tuple contains: (Symbol, Issue Name).
""" """
df = pd.concat([self.stock.symbol_list, self.crypto.symbol_list]) df = pd.concat([self.stock.symbol_list, self.crypto.symbol_list])
search = search.lower() search = search.lower()
df["Match"] = df.apply( df["Match"] = df.apply(
lambda x: fuzz.ratio(search, f"{x['symbol']}".lower()), lambda x: fuzz.ratio(search, f"{x['symbol']}".lower()),
axis=1, axis=1,
) )
df.sort_values(by="Match", ascending=False, inplace=True) df.sort_values(by="Match", ascending=False, inplace=True)
# if df["Match"].head().sum() < 300: # if df["Match"].head().sum() < 300:
# df["Match"] = df.apply( # df["Match"] = df.apply(
# lambda x: fuzz.partial_ratio(search, x["name"].lower()), # lambda x: fuzz.partial_ratio(search, x["name"].lower()),
# axis=1, # axis=1,
# ) # )
# df.sort_values(by="Match", ascending=False, inplace=True) # df.sort_values(by="Match", ascending=False, inplace=True)
symbols = df.head(20) symbols = df.head(20)
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 self.searched_symbols[search] = symbol_list
return symbol_list return symbol_list
def inline_search(self, search: str) -> list[tuple[str, str]]: def inline_search(self, search: str) -> list[tuple[str, str]]:
"""Searches based on the shortest symbol that contains the same string as the search. """Searches based on the shortest symbol that contains the same string as the search.
Should be very fast compared to a fuzzy search. Should be very fast compared to a fuzzy search.
Parameters Parameters
---------- ----------
search : str search : str
String used to match against symbols. String used to match against symbols.
Returns Returns
------- -------
list[tuple[str, str]] list[tuple[str, str]]
Each tuple contains: (Symbol, Issue Name). Each tuple contains: (Symbol, Issue Name).
""" """
df = pd.concat([self.stock.symbol_list, self.crypto.symbol_list]) df = pd.concat([self.stock.symbol_list, self.crypto.symbol_list])
search = search.lower() search = search.lower()
df = df[df["type_id"].str.contains(search, regex=False)].sort_values( df = df[df["type_id"].str.contains(search, regex=False)].sort_values(
by="type_id", key=lambda x: x.str.len() by="type_id", key=lambda x: x.str.len()
) )
symbols = df.head(20) symbols = df.head(20)
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 self.searched_symbols[search] = symbol_list
return symbol_list return symbol_list
def price_reply(self, symbols: list[Symbol]) -> list[str]: def price_reply(self, symbols: list[Symbol]) -> list[str]:
"""Returns current market price or after hours if its available for a given stock symbol. """Returns current market price or after hours if its available for a given stock symbol.
Parameters Parameters
---------- ----------
symbols : list symbols : list
List of stock symbols. List of stock symbols.
Returns Returns
------- -------
Dict[str, str] Dict[str, str]
Each symbol passed in is a key with its value being a human readable Each symbol passed in is a key with its value being a human readable
markdown formatted string of the symbols price and movement. markdown formatted string of the symbols price and movement.
""" """
replies = [] replies = []
for symbol in symbols: for symbol in symbols:
info(symbol) info(symbol)
if isinstance(symbol, Stock): if isinstance(symbol, Stock):
replies.append(self.stock.price_reply(symbol)) replies.append(self.stock.price_reply(symbol))
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
replies.append(self.crypto.price_reply(symbol)) replies.append(self.crypto.price_reply(symbol))
else: else:
info(f"{symbol} is not a Stock or Coin") info(f"{symbol} is not a Stock or Coin")
return replies return replies
def dividend_reply(self, symbols: list) -> list[str]: def dividend_reply(self, symbols: list) -> list[str]:
"""Returns the most recent, or next dividend date for a stock symbol. """Returns the most recent, or next dividend date for a stock symbol.
Parameters Parameters
---------- ----------
symbols : list symbols : list
List of stock symbols. List of stock symbols.
Returns Returns
------- -------
Dict[str, str] Dict[str, str]
Each symbol passed in is a key with its value being a human readable Each symbol passed in is a key with its value being a human readable
formatted string of the symbols div dates. formatted string of the symbols div dates.
""" """
replies = [] replies = []
for symbol in symbols: for symbol in symbols:
if isinstance(symbol, Stock): if isinstance(symbol, Stock):
replies.append(self.stock.dividend_reply(symbol)) replies.append(self.stock.dividend_reply(symbol))
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
replies.append("Cryptocurrencies do no have Dividends.") replies.append("Cryptocurrencies do no have Dividends.")
else: else:
debug(f"{symbol} is not a Stock or Coin") debug(f"{symbol} is not a Stock or Coin")
return replies return replies
def news_reply(self, symbols: list) -> list[str]: def news_reply(self, symbols: list) -> list[str]:
"""Gets recent english news on stock symbols. """Gets recent english news on stock symbols.
Parameters Parameters
---------- ----------
symbols : list symbols : list
List of stock symbols. List of stock symbols.
Returns Returns
------- -------
Dict[str, str] Dict[str, str]
Each symbol passed in is a key with its value being a human Each symbol passed in is a key with its value being a human
readable markdown formatted string of the symbols news. readable markdown formatted string of the symbols news.
""" """
replies = [] replies = []
for symbol in symbols: for symbol in symbols:
if isinstance(symbol, Stock): if isinstance(symbol, Stock):
replies.append(self.stock.news_reply(symbol)) replies.append(self.stock.news_reply(symbol))
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
# replies.append(self.crypto.news_reply(symbol)) # replies.append(self.crypto.news_reply(symbol))
replies.append( replies.append(
"News is not yet supported for cryptocurrencies. If you have any suggestions for news sources please contatct @MisterBiggs" "News is not yet supported for cryptocurrencies. If you have any suggestions for news sources please contatct @MisterBiggs"
) )
else: else:
debug(f"{symbol} is not a Stock or Coin") debug(f"{symbol} is not a Stock or Coin")
return replies return replies
def info_reply(self, symbols: list) -> list[str]: def info_reply(self, symbols: list) -> list[str]:
"""Gets information on stock symbols. """Gets information on stock symbols.
Parameters Parameters
---------- ----------
symbols : list[str] symbols : list[str]
List of stock symbols. List of stock symbols.
Returns Returns
------- -------
Dict[str, str] Dict[str, str]
Each symbol passed in is a key with its value being a human readable formatted Each symbol passed in is a key with its value being a human readable formatted
string of the symbols information. string of the symbols information.
""" """
replies = [] replies = []
for symbol in symbols: for symbol in symbols:
if isinstance(symbol, Stock): if isinstance(symbol, Stock):
replies.append(self.stock.info_reply(symbol)) replies.append(self.stock.info_reply(symbol))
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
replies.append(self.crypto.info_reply(symbol)) replies.append(self.crypto.info_reply(symbol))
else: else:
debug(f"{symbol} is not a Stock or Coin") debug(f"{symbol} is not a Stock or Coin")
return replies return replies
def intra_reply(self, symbol: Symbol) -> pd.DataFrame: def intra_reply(self, symbol: Symbol) -> pd.DataFrame:
"""Returns price data for a symbol since the last market open. """Returns price data for a symbol since the last market open.
Parameters Parameters
---------- ----------
symbol : str symbol : str
Stock symbol. Stock symbol.
Returns Returns
------- -------
pd.DataFrame pd.DataFrame
Returns a timeseries dataframe with high, low, and volume data if its available. Returns a timeseries dataframe with high, low, and volume data if its available.
Otherwise returns empty pd.DataFrame. Otherwise returns empty pd.DataFrame.
""" """
if isinstance(symbol, Stock): if isinstance(symbol, Stock):
return self.stock.intra_reply(symbol) return self.stock.intra_reply(symbol)
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
return self.crypto.intra_reply(symbol) return self.crypto.intra_reply(symbol)
else: else:
debug(f"{symbol} is not a Stock or Coin") debug(f"{symbol} is not a Stock or Coin")
return pd.DataFrame() return pd.DataFrame()
def chart_reply(self, symbol: Symbol) -> 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. """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. Also caches multiple requests made in the same day.
Parameters Parameters
---------- ----------
symbol : str symbol : str
Stock symbol. Stock symbol.
Returns Returns
------- -------
pd.DataFrame pd.DataFrame
Returns a timeseries dataframe with high, low, and volume data if its available. Returns a timeseries dataframe with high, low, and volume data if its available.
Otherwise returns empty pd.DataFrame. Otherwise returns empty pd.DataFrame.
""" """
if isinstance(symbol, Stock): if isinstance(symbol, Stock):
return self.stock.chart_reply(symbol) return self.stock.chart_reply(symbol)
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
return self.crypto.chart_reply(symbol) return self.crypto.chart_reply(symbol)
else: else:
debug(f"{symbol} is not a Stock or Coin") debug(f"{symbol} is not a Stock or Coin")
return pd.DataFrame() return pd.DataFrame()
def stat_reply(self, symbols: list[Symbol]) -> list[str]: def stat_reply(self, symbols: list[Symbol]) -> list[str]:
"""Gets key statistics for each symbol in the list """Gets key statistics for each symbol in the list
Parameters Parameters
---------- ----------
symbols : list[str] symbols : list[str]
List of stock symbols List of stock symbols
Returns Returns
------- -------
Dict[str, str] Dict[str, str]
Each symbol passed in is a key with its value being a human readable Each symbol passed in is a key with its value being a human readable
formatted string of the symbols statistics. formatted string of the symbols statistics.
""" """
replies = [] replies = []
for symbol in symbols: for symbol in symbols:
if isinstance(symbol, Stock): if isinstance(symbol, Stock):
replies.append(self.stock.stat_reply(symbol)) replies.append(self.stock.stat_reply(symbol))
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
replies.append(self.crypto.stat_reply(symbol)) replies.append(self.crypto.stat_reply(symbol))
else: else:
debug(f"{symbol} is not a Stock or Coin") debug(f"{symbol} is not a Stock or Coin")
return replies return replies
def cap_reply(self, symbols: list[Symbol]) -> list[str]: def cap_reply(self, symbols: list[Symbol]) -> list[str]:
"""Gets market cap for each symbol in the list """Gets market cap for each symbol in the list
Parameters Parameters
---------- ----------
symbols : list[str] symbols : list[str]
List of stock symbols List of stock symbols
Returns Returns
------- -------
Dict[str, str] Dict[str, str]
Each symbol passed in is a key with its value being a human readable Each symbol passed in is a key with its value being a human readable
formatted string of the symbols market cap. formatted string of the symbols market cap.
""" """
replies = [] replies = []
for symbol in symbols: for symbol in symbols:
if isinstance(symbol, Stock): if isinstance(symbol, Stock):
replies.append(self.stock.cap_reply(symbol)) replies.append(self.stock.cap_reply(symbol))
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
replies.append(self.crypto.cap_reply(symbol)) replies.append(self.crypto.cap_reply(symbol))
else: else:
debug(f"{symbol} is not a Stock or Coin") debug(f"{symbol} is not a Stock or Coin")
return replies return replies
def spark_reply(self, symbols: list[Symbol]) -> list[str]: def spark_reply(self, symbols: list[Symbol]) -> list[str]:
"""Gets change for each symbol and returns it in a compact format """Gets change for each symbol and returns it in a compact format
Parameters Parameters
---------- ----------
symbols : list[str] symbols : list[str]
List of stock symbols List of stock symbols
Returns Returns
------- -------
list[str] list[str]
List of human readable strings. List of human readable strings.
""" """
replies = [] replies = []
for symbol in symbols: for symbol in symbols:
if isinstance(symbol, Stock): if isinstance(symbol, Stock):
replies.append(self.stock.spark_reply(symbol)) replies.append(self.stock.spark_reply(symbol))
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
replies.append(self.crypto.spark_reply(symbol)) replies.append(self.crypto.spark_reply(symbol))
else: else:
debug(f"{symbol} is not a Stock or Coin") debug(f"{symbol} is not a Stock or Coin")
return replies return replies
@cached(cache=TTLCache(maxsize=1024, ttl=600)) @cached(cache=TTLCache(maxsize=1024, ttl=600))
def trending(self) -> str: def trending(self) -> str:
"""Checks APIs for trending symbols. """Checks APIs for trending symbols.
Returns Returns
------- -------
list[str] list[str]
List of preformatted strings to be sent to user. List of preformatted strings to be sent to user.
""" """
stocks = self.stock.trending() stocks = self.stock.trending()
coins = self.crypto.trending() coins = self.crypto.trending()
reply = "" reply = ""
if self.trending_count: if self.trending_count:
reply += "🔥Trending on the Stock Bot:\n`" reply += "🔥Trending on the Stock Bot:\n`"
reply += "" * len("Trending on the Stock Bot:") + "`\n" reply += "" * len("Trending on the Stock Bot:") + "`\n"
sorted_trending = [ sorted_trending = [
s[0] s[0]
for s in sorted(self.trending_count.items(), key=lambda item: item[1]) for s in sorted(self.trending_count.items(), key=lambda item: item[1])
][::-1][0:5] ][::-1][0:5]
for t in sorted_trending: for t in sorted_trending:
reply += self.spark_reply(self.find_symbols(t))[0] + "\n" reply += self.spark_reply(self.find_symbols(t))[0] + "\n"
if stocks: if stocks:
reply += "\n\n💵Trending Stocks:\n`" reply += "\n\n💵Trending Stocks:\n`"
reply += "" * len("Trending Stocks:") + "`\n" reply += "" * len("Trending Stocks:") + "`\n"
for stock in stocks: for stock in stocks:
reply += stock + "\n" reply += stock + "\n"
if coins: if coins:
reply += "\n\n🦎Trending Crypto:\n`" reply += "\n\n🦎Trending Crypto:\n`"
reply += "" * len("Trending Crypto:") + "`\n" reply += "" * len("Trending Crypto:") + "`\n"
for coin in coins: for coin in coins:
reply += coin + "\n" reply += coin + "\n"
if "`$GME" in reply: if "`$GME" in reply:
reply = reply.replace("🔥", "🦍") reply = reply.replace("🔥", "🦍")
if reply: if reply:
return reply return reply
else: else:
warning("Failed to collect trending data.") warning("Failed to collect trending data.")
return "Trending data is not currently available." return "Trending data is not currently available."
def random_pick(self) -> str: def random_pick(self) -> str:
choice = random.choice( choice = random.choice(
list(self.stock.symbol_list["description"]) list(self.stock.symbol_list["description"])
+ list(self.crypto.symbol_list["description"]) + list(self.crypto.symbol_list["description"])
) )
hold = ( hold = (
datetime.date.today() + datetime.timedelta(random.randint(1, 365)) datetime.date.today() + datetime.timedelta(random.randint(1, 365))
).strftime("%b %d, %Y") ).strftime("%b %d, %Y")
return f"{choice}\nBuy and hold until: {hold}" return f"{choice}\nBuy and hold until: {hold}"
def batch_price_reply(self, symbols: list[Symbol]) -> list[str]: def batch_price_reply(self, symbols: list[Symbol]) -> list[str]:
"""Returns current market price or after hours if its available for a given stock symbol. """Returns current market price or after hours if its available for a given stock symbol.
Parameters Parameters
---------- ----------
symbols : list symbols : list
List of stock symbols. List of stock symbols.
Returns Returns
------- -------
Dict[str, str] Dict[str, str]
Each symbol passed in is a key with its value being a human readable Each symbol passed in is a key with its value being a human readable
markdown formatted string of the symbols price and movement. markdown formatted string of the symbols price and movement.
""" """
replies = [] replies = []
stocks = [] stocks = []
coins = [] coins = []
for symbol in symbols: for symbol in symbols:
if isinstance(symbol, Stock): if isinstance(symbol, Stock):
stocks.append(symbol) stocks.append(symbol)
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
coins.append(symbol) coins.append(symbol)
else: else:
debug(f"{symbol} is not a Stock or Coin") debug(f"{symbol} is not a Stock or Coin")
if stocks: if stocks:
# IEX batch endpoint doesnt seem to be working right now # IEX batch endpoint doesnt seem to be working right now
for stock in stocks: for stock in stocks:
replies.append(self.stock.price_reply(stock)) replies.append(self.stock.price_reply(stock))
if coins: if coins:
replies = replies + self.crypto.batch_price(coins) replies = replies + self.crypto.batch_price(coins)
return replies return replies