diff --git a/IEX_Symbol.py b/IEX_Symbol.py index 4acf549..0012efd 100644 --- a/IEX_Symbol.py +++ b/IEX_Symbol.py @@ -55,10 +55,12 @@ class IEX_Symbol: ) -> Optional[Tuple[pd.DataFrame, datetime]]: reg_symbols = r.get( - f"https://cloud.iexapis.com/stable/ref-data/symbols?token={self.IEX_TOKEN}" + f"https://cloud.iexapis.com/stable/ref-data/symbols?token={self.IEX_TOKEN}", + timeout=5, ).json() otc_symbols = r.get( - f"https://cloud.iexapis.com/stable/ref-data/otc/symbols?token={self.IEX_TOKEN}" + f"https://cloud.iexapis.com/stable/ref-data/otc/symbols?token={self.IEX_TOKEN}", + timeout=5, ).json() reg = pd.DataFrame(data=reg_symbols) @@ -69,8 +71,9 @@ class IEX_Symbol: symbols["description"] = "$" + symbols["symbol"] + ": " + symbols["name"] symbols["id"] = symbols["symbol"] + symbols["type_id"] = "$" + symbols["symbol"].str.lower() - symbols = symbols[["id", "symbol", "name", "description"]] + symbols = symbols[["id", "symbol", "name", "description", "type_id"]] self.symbol_list = symbols if return_df: return symbols, datetime.now() @@ -83,7 +86,10 @@ class IEX_Symbol: str Human readable text on status of IEX API """ - resp = r.get("https://pjmps0c34hp7.statuspage.io/api/v2/status.json") + resp = r.get( + "https://pjmps0c34hp7.statuspage.io/api/v2/status.json", + timeout=5, + ) if resp.status_code == 200: status = resp.json()["status"] @@ -155,7 +161,10 @@ class IEX_Symbol: IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol.id}/quote?token={self.IEX_TOKEN}" - response = r.get(IEXurl) + response = r.get( + IEXurl, + timeout=5, + ) if response.status_code == 200: IEXData = response.json() @@ -226,7 +235,10 @@ class IEX_Symbol: return "OTC stocks do not currently support any commands." IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/dividends/next?token={self.IEX_TOKEN}" - response = r.get(IEXurl) + response = r.get( + IEXurl, + timeout=5, + ) if response.status_code == 200 and response.json(): IEXData = response.json()[0] keys = ( @@ -288,7 +300,10 @@ class IEX_Symbol: return "OTC stocks do not currently support any commands." IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/news/last/15?token={self.IEX_TOKEN}" - response = r.get(IEXurl) + response = r.get( + IEXurl, + timeout=5, + ) if response.status_code == 200: data = response.json() if data: @@ -324,7 +339,10 @@ class IEX_Symbol: return "OTC stocks do not currently support any commands." IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/company?token={self.IEX_TOKEN}" - response = r.get(IEXurl) + response = r.get( + IEXurl, + timeout=5, + ) if response.status_code == 200: data = response.json() @@ -352,7 +370,10 @@ class IEX_Symbol: return "OTC stocks do not currently support any commands." IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/stats?token={self.IEX_TOKEN}" - response = r.get(IEXurl) + response = r.get( + IEXurl, + timeout=5, + ) if response.status_code == 200: data = response.json() @@ -362,7 +383,7 @@ class IEX_Symbol: if "companyName" in data: m += f"Company Name: {data['companyName']}\n" if "marketcap" in data: - m += f"Market Cap: {data['marketcap']:,}\n" + m += f"Market Cap: ${data['marketcap']:,}\n" if "week52high" in data: m += f"52 Week (high-low): {data['week52high']:,} " if "week52low" in data: @@ -399,7 +420,10 @@ class IEX_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( + IEXurl, + timeout=5, + ) if response.status_code == 200: df = pd.DataFrame(response.json()) df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"]) @@ -437,7 +461,8 @@ class IEX_Symbol: pass response = r.get( - f"https://cloud.iexapis.com/stable/stock/{symbol}/chart/1mm?token={self.IEX_TOKEN}&chartInterval=3&includeToday=false" + f"https://cloud.iexapis.com/stable/stock/{symbol}/chart/1mm?token={self.IEX_TOKEN}&chartInterval=3&includeToday=false", + timeout=5, ) if response.status_code == 200: @@ -460,7 +485,8 @@ class IEX_Symbol: """ stocks = r.get( - f"https://cloud.iexapis.com/stable/stock/market/list/mostactive?token={self.IEX_TOKEN}" + f"https://cloud.iexapis.com/stable/stock/market/list/mostactive?token={self.IEX_TOKEN}", + timeout=5, ).json() return [f"${s['symbol']}: {s['companyName']}" for s in stocks] diff --git a/bot.py b/bot.py index a8a6207..e14f42b 100644 --- a/bot.py +++ b/bot.py @@ -98,17 +98,9 @@ def donate(update: Update, context: CallbackContext): parse_mode=telegram.ParseMode.MARKDOWN, disable_notification=True, ) - return + amount = 1 else: amount = update.message.text.replace("/donate", "").replace("$", "").strip() - title = "Simple Stock Bot Donation" - description = f"Simple Stock Bot Donation of ${amount}" - payload = "simple-stock-bot" - provider_token = STRIPE_TOKEN - start_parameter = str(chat_id) - - print(start_parameter) - currency = "USD" try: price = int(float(amount) * 100) @@ -117,32 +109,38 @@ def donate(update: Update, context: CallbackContext): return print(price) - prices = [LabeledPrice("Donation:", price)] - context.bot.send_invoice( - chat_id, - title, - description, - payload, - provider_token, - start_parameter, - currency, - prices, + chat_id=chat_id, + title="Simple Stock Bot Donation", + description=f"Simple Stock Bot Donation of ${amount}", + payload=f"simple-stock-bot-{chat_id}", + provider_token=STRIPE_TOKEN, + currency="USD", + prices=[LabeledPrice("Donation:", price)], + start_parameter="", + # suggested_tip_amounts=[100, 500, 1000, 2000], + photo_url="https://simple-stock-bots.gitlab.io/site/img/Telegram.png", + photo_width=500, + photo_height=500, ) def precheckout_callback(update: Update, context: CallbackContext): query = update.pre_checkout_query - if query.invoice_payload == "simple-stock-bot": - # answer False pre_checkout_query - query.answer(ok=True) - else: - query.answer(ok=False, error_message="Something went wrong...") + query.answer(ok=True) + # I dont think I need to check since its only donations. + # if query.invoice_payload == "simple-stock-bot": + # # answer False pre_checkout_query + # query.answer(ok=True) + # else: + # query.answer(ok=False, error_message="Something went wrong...") def successful_payment_callback(update: Update, context: CallbackContext): - update.message.reply_text("Thank you for your donation!") + update.message.reply_text( + "Thank you for your donation! It goes a long way to keeping the bot free!" + ) def symbol_detect(update: Update, context: CallbackContext): @@ -307,16 +305,15 @@ def intra(update: Update, context: CallbackContext): title=f"\n{symbol.name}", volume="volume" in df.keys(), style="yahoo", - mav=20, savefig=dict(fname=buf, dpi=400, bbox_inches="tight"), ) buf.seek(0) update.message.reply_photo( photo=buf, - 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]}", + caption=f"\nIntraday chart for {symbol.name} from {df.first_valid_index().strftime('%d %b at %H:%M')} to" + + f" {df.last_valid_index().strftime('%d %b at %H:%M')}" + + f"\n\n{s.price_reply([symbol])[0]}", parse_mode=telegram.ParseMode.MARKDOWN, disable_notification=True, ) @@ -422,12 +419,11 @@ def inline_query(update: Update, context: CallbackContext): Does a fuzzy search on input and returns stocks that are close. """ - matches = s.search_symbols(update.inline_query.query)[:] + matches = s.inline_search(update.inline_query.query)[:5] symbols = " ".join([match[1].split(":")[0] for match in matches]) prices = s.batch_price_reply(s.find_symbols(symbols)) - # print(len(matches), len(prices)) - # print(prices) + results = [] print(update.inline_query.query) for match, price in zip(matches, prices): @@ -469,19 +465,22 @@ def error(update: Update, context: CallbackContext): ) 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)}" - ) + # 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)}" + # ) + update.message.reply_text( + text="An error has occured. Please inform @MisterBiggs if the error persists." + ) - # 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.") def main(): diff --git a/cg_Crypto.py b/cg_Crypto.py index e1f9ad8..6413040 100644 --- a/cg_Crypto.py +++ b/cg_Crypto.py @@ -44,11 +44,17 @@ class cg_Crypto: self, return_df=False ) -> Optional[Tuple[pd.DataFrame, datetime]]: - raw_symbols = r.get("https://api.coingecko.com/api/v3/coins/list").json() + raw_symbols = r.get( + "https://api.coingecko.com/api/v3/coins/list", + timeout=5, + ).json() symbols = pd.DataFrame(data=raw_symbols) - symbols["description"] = "$$" + symbols["symbol"] + ": " + symbols["name"] + symbols["description"] = ( + "$$" + symbols["symbol"].str.upper() + ": " + symbols["name"] + ) symbols = symbols[["id", "symbol", "name", "description"]] + symbols["type_id"] = "$$" + symbols["id"] self.symbol_list = symbols if return_df: @@ -62,7 +68,10 @@ class cg_Crypto: str Human readable text on status of CoinGecko API """ - status = r.get("https://api.coingecko.com/api/v3/ping") + status = r.get( + "https://api.coingecko.com/api/v3/ping", + timeout=5, + ) if status.status_code == 200: return f"CoinGecko API responded that it was OK in {status.elapsed.total_seconds()} Seconds." @@ -124,7 +133,8 @@ class cg_Crypto: """ response = r.get( - f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false" + f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false", + timeout=5, ) if response.status_code == 200: data = response.json() @@ -167,7 +177,8 @@ class cg_Crypto: Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. """ response = r.get( - f"https://api.coingecko.com/api/v3/coins/{symbol.id}/ohlc?vs_currency=usd&days=1" + f"https://api.coingecko.com/api/v3/coins/{symbol.id}/ohlc?vs_currency=usd&days=1", + timeout=5, ) if response.status_code == 200: df = pd.DataFrame( @@ -194,7 +205,8 @@ class cg_Crypto: Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. """ response = r.get( - f"https://api.coingecko.com/api/v3/coins/{symbol.id}/ohlc?vs_currency=usd&days=30" + f"https://api.coingecko.com/api/v3/coins/{symbol.id}/ohlc?vs_currency=usd&days=30", + timeout=5, ) if response.status_code == 200: @@ -221,14 +233,16 @@ 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.id}?localization=false" + f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false", + timeout=5, ) if response.status_code == 200: data = response.json() return f""" [{data['name']}]({data['links']['homepage'][0]}) Statistics: - Maket Cap Ranking: {data.get('market_cap_rank',"Not Available")} + Market Cap: ${data['market_data']['market_cap'][self.vs_currency]:,} + Market 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')} @@ -253,7 +267,8 @@ class cg_Crypto: """ response = r.get( - f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false" + f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false", + timeout=5, ) if response.status_code == 200: data = response.json() @@ -273,9 +288,10 @@ class cg_Crypto: list of $$ID: NAME """ - coins = r.get("https://api.coingecko.com/api/v3/search/trending").json()[ - "coins" - ] + coins = r.get( + "https://api.coingecko.com/api/v3/search/trending", + timeout=5, + ).json()["coins"] return [f"$${c['item']['symbol'].upper()}: {c['item']['name']}" for c in coins] @@ -283,23 +299,20 @@ class cg_Crypto: query = ",".join([c.id for c in coins]) prices = r.get( - f"https://api.coingecko.com/api/v3/simple/price?ids={query}&vs_currencies=usd&include_24hr_change=true" + f"https://api.coingecko.com/api/v3/simple/price?ids={query}&vs_currencies=usd&include_24hr_change=true", + timeout=5, ).json() replies = [] - for name, val in prices.items(): - if price := val.get("usd"): - price = val.get("usd") - else: - replies.append(f"{name} price data unavailable.") - break + for coin in coins: + if coin.id in prices: + p = prices[coin.id] - change = 0 - if val.get("usd_24h_change") is not None: - change = val.get("usd_24h_change") + if p.get("usd_24h_change") is None: + p["usd_24h_change"] = 0 - replies.append( - f"{name}: ${price:,} and has moved {change:.2f}% in the past 24 hours." - ) + replies.append( + f"{coin.name}: ${p.get('usd',0):,} and has moved {p.get('usd_24h_change',0.0):.2f}% in the past 24 hours." + ) return replies diff --git a/requirements.txt b/requirements.txt index 74ecaef..45e4aa1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -python-telegram-bot==13.2 +python-telegram-bot==13.5 requests==2.25.1 pandas==1.2.1 fuzzywuzzy==0.18.0 diff --git a/symbol_router.py b/symbol_router.py index bbca1ed..10ca415 100644 --- a/symbol_router.py +++ b/symbol_router.py @@ -100,13 +100,41 @@ class Router: ) df.sort_values(by="Match", ascending=False, inplace=True) - if df["Match"].head().sum() < 300: - df["Match"] = df.apply( - lambda x: fuzz.partial_ratio(search, x["name"].lower()), - axis=1, - ) + # if df["Match"].head().sum() < 300: + # df["Match"] = df.apply( + # lambda x: fuzz.partial_ratio(search, x["name"].lower()), + # axis=1, + # ) - df.sort_values(by="Match", ascending=False, inplace=True) + # df.sort_values(by="Match", ascending=False, inplace=True) + + symbols = df.head(20) + symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"]))) + self.searched_symbols[search] = symbol_list + return symbol_list + + def inline_search(self, search: str) -> List[Tuple[str, str]]: + """Searches based on the shortest symbol that contains the same string as the search. + Should be very fast compared to a fuzzy search. + + Parameters + ---------- + search : str + String used to match against symbols. + + Returns + ------- + List[tuple[str, str]] + Each tuple contains: (Symbol, Issue Name). + """ + + df = pd.concat([self.stock.symbol_list, self.crypto.symbol_list]) + + search = search.lower() + + df = df[df["type_id"].str.contains(search, regex=False)].sort_values( + by="type_id", key=lambda x: x.str.len() + ) symbols = df.head(20) symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"]))) @@ -184,7 +212,9 @@ class Router: 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.") + replies.append( + "News is not yet supported for cryptocurrencies. If you have any suggestions for news sources please contatct @MisterBiggs" + ) else: print(f"{symbol} is not a Stock or Coin") @@ -347,11 +377,10 @@ class Router: print(f"{symbol} is not a Stock or Coin") if stocks: - for ( - stock - ) in 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: replies.append(self.stock.price_reply(stock)) if coins: - replies.append(self.crypto.batch_price(coins)) + replies = replies + self.crypto.batch_price(coins) return replies