diff --git a/IEX_Symbol.py b/IEX_Symbol.py index 0012efd..4135002 100644 --- a/IEX_Symbol.py +++ b/IEX_Symbol.py @@ -1,14 +1,15 @@ """Class with functions for running the bot with IEX Cloud. """ +import os from datetime import datetime -from typing import Optional, List, Tuple +from logging import warning +from typing import List, Optional, Tuple import pandas as pd import requests as r import schedule from fuzzywuzzy import fuzz -import os from Symbol import Stock @@ -30,13 +31,13 @@ class IEX_Symbol: Parameters ---------- IEX_TOKEN : str - IEX Token + IEX API Token """ try: self.IEX_TOKEN = os.environ["IEX"] except KeyError: self.IEX_TOKEN = "" - print( + warning( "Starting without an IEX Token will not allow you to get market data!" ) @@ -47,12 +48,28 @@ class IEX_Symbol: schedule.every().day.do(self.clear_charts) def clear_charts(self) -> None: - """Clears cache of chart data.""" + """ + Clears cache of chart data. + Charts are cached so that only 1 API call per 24 hours is needed since the + chart data is expensive and a large download. + """ self.charts = {} def get_symbol_list( self, return_df=False ) -> Optional[Tuple[pd.DataFrame, datetime]]: + """Gets list of all symbols supported by IEX + + Parameters + ---------- + return_df : bool, optional + return the dataframe of all stock symbols, by default False + + Returns + ------- + Optional[Tuple[pd.DataFrame, datetime]] + If `return_df` is set to `True` returns a dataframe, otherwise returns `None`. + """ reg_symbols = r.get( f"https://cloud.iexapis.com/stable/ref-data/symbols?token={self.IEX_TOKEN}", @@ -145,18 +162,16 @@ class IEX_Symbol: return symbol_list def price_reply(self, symbol: Stock) -> str: - """Returns current market price or after hours if its available for a given stock symbol. + """Returns price movement of Stock for the last market day, or after hours. Parameters ---------- - symbols : list - List of stock symbols. + symbol : Stock 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. + str + Formatted markdown """ IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol.id}/quote?token={self.IEX_TOKEN}" @@ -223,13 +238,12 @@ class IEX_Symbol: Parameters ---------- - symbols : list - List of stock symbols. + symbol : Stock 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. + str + Formatted markdown """ if symbol.symbol.upper() in self.otc_list: return "OTC stocks do not currently support any commands." @@ -284,17 +298,16 @@ class IEX_Symbol: return f"${symbol.id.upper()} either doesn't exist or pays no dividend." def news_reply(self, symbol: Stock) -> str: - """Gets recent english news on stock symbols. + """Gets most recent, english, non-paywalled news Parameters ---------- - symbols : list - List of stock symbols. + symbol : Stock 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. + str + Formatted markdown """ if symbol.symbol.upper() in self.otc_list: return "OTC stocks do not currently support any commands." @@ -323,17 +336,16 @@ class IEX_Symbol: return f"News for **{symbol.id.upper()}**:\n" + "\n".join(line[:5]) def info_reply(self, symbol: Stock) -> str: - """Gets information on stock symbols. + """Gets description for Stock Parameters ---------- - symbols : List[str] - List of stock symbols. + symbol : Stock Returns ------- - Dict[str, str] - Each symbol passed in is a key with its value being a human readable formatted string of the symbols information. + str + Formatted text """ if symbol.symbol.upper() in self.otc_list: return "OTC stocks do not currently support any commands." @@ -354,17 +366,16 @@ class IEX_Symbol: return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." def stat_reply(self, symbol: Stock) -> str: - """Gets key statistics for each symbol in the list + """Key statistics on a Stock Parameters ---------- - symbols : List[str] - List of stock symbols + symbol : Stock Returns ------- - Dict[str, str] - Each symbol passed in is a key with its value being a human readable formatted string of the symbols statistics. + str + Formatted markdown """ if symbol.symbol.upper() in self.otc_list: return "OTC stocks do not currently support any commands." @@ -400,6 +411,28 @@ class IEX_Symbol: else: return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." + def cap_reply(self, stock: Stock) -> str: + """Get the Market Cap of a stock""" + response = r.get( + f"https://cloud.iexapis.com/stable/stock/{stock.id}/stats?token={self.IEX_TOKEN}", + timeout=5, + ) + if response.status_code == 200: + + try: + data = response.json() + + cap = data["marketcap"] + except KeyError: + return f"{stock.id} returned an error." + + message = f"The current market cap of {stock.name} is $**{cap:,.2f}**" + + else: + message = f"The Coin: {stock.name} was not found or returned and error." + + return message + def intra_reply(self, symbol: Stock) -> pd.DataFrame: """Returns price data for a symbol since the last market open. @@ -476,17 +509,22 @@ class IEX_Symbol: return pd.DataFrame() def trending(self) -> list[str]: - """Gets current coins trending on coingecko + """Gets current coins trending on IEX. Only returns when market is open. Returns ------- list[str] - list of $$ID: NAME + list of $ID: NAME, CHANGE% """ stocks = r.get( 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] + ) + if stocks.status_code == 200: + return [ + f"`${s['symbol']}`: {s['companyName']}, {s['changePercent']:.2f}%" + for s in stocks.json() + ] + else: + return ["Trending Stocks Currently Unavailable."] diff --git a/Symbol.py b/Symbol.py index 3e204c6..db04b4a 100644 --- a/Symbol.py +++ b/Symbol.py @@ -1,4 +1,5 @@ import functools + import requests as r @@ -25,6 +26,8 @@ class Symbol: class Stock(Symbol): + """Stock Market Object. Gets data from IEX Cloud""" + def __init__(self, symbol: str) -> None: self.symbol = symbol self.id = symbol @@ -36,6 +39,8 @@ coins = r.get("https://api.coingecko.com/api/v3/coins/list").json() class Coin(Symbol): + """Cryptocurrency Object. Gets data from CoinGecko.""" + @functools.cache def __init__(self, symbol: str) -> None: self.symbol = symbol diff --git a/T_info.py b/T_info.py index 66d07ef..1ba184b 100644 --- a/T_info.py +++ b/T_info.py @@ -2,6 +2,7 @@ """ import re + import requests as r @@ -19,9 +20,9 @@ Thanks for using this bot, consider supporting it by [buying me a beer.](https:/ Keep up with the latest news for the bot in its Telegram Channel: https://t.me/simplestockbotnews -Full documentation on using and running your own stock bot can be found [on the bots website.](https://simple-stock-bots.gitlab.io/site) +Full documentation on using and running your own stock bot can be found on the bots [docs.](https://docs.simplestockbot.com) -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. +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. @@ -33,6 +34,7 @@ Simply calling a symbol in any message that the bot can see will also return the - `/news $[symbol]` News about the symbol. 📰 - `/info $[symbol]` General information about the symbol. â„šī¸ - `/stat $[symbol]` Key statistics about the symbol. đŸ”ĸ + - `/cap $[symbol]` Market Capitalization of symbol. 💰 - `/trending` Trending Stocks and Cryptos. đŸ’Ŧ - `/help` Get some help using the bot. 🆘 @@ -41,7 +43,7 @@ Simply calling a symbol in any message that the bot can see will also return the Market data is provided by [IEX Cloud](https://iexcloud.io) - If you believe the bot is not behaving properly run `/status`. + If you believe the bot is not behaving properly run `/status` or [get in touch](https://docs.simplestockbot.com/contact). """ donate_text = """ @@ -54,9 +56,8 @@ The easiest way to donate is to run the `/donate [amount in USD]` command with U Example: `/donate 2` would donate 2 USD. An alternative way to donate is through https://www.buymeacoffee.com/Anson which requires no account and 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 see the [website](https:docs.simplestockbot.com) -_Donations can only be made in a chat directly with @simplestockbot_ """ @@ -66,8 +67,9 @@ 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. đŸ”ĸ +cap - $[symbol] Market Capitalization of symbol. 💰 dividend - $[symbol] Dividend info 📅 -intra - $[symbol] Plot since the last market open. 📈 trending - Trending Stocks and Cryptos. đŸ’Ŧ +intra - $[symbol] Plot since the last market open. 📈 chart - $[chart] Plot of the past month. 📊 """ # Not used by the bot but for updaing commands with BotFather diff --git a/bot.py b/bot.py index e14f42b..f18e638 100644 --- a/bot.py +++ b/bot.py @@ -1,15 +1,17 @@ # Works with Python 3.8 import datetime +import html import io +import json import logging import os -import html -import json +import random +import string import traceback - -import mplfinance as mpf +from logging import critical, debug, error, info, warning from uuid import uuid4 +import mplfinance as mpf import telegram from telegram import ( InlineQueryResultArticle, @@ -18,13 +20,13 @@ from telegram import ( Update, ) from telegram.ext import ( + CallbackContext, CommandHandler, Filters, InlineQueryHandler, MessageHandler, PreCheckoutQueryHandler, Updater, - CallbackContext, ) from symbol_router import Router @@ -36,7 +38,7 @@ try: STRIPE_TOKEN = os.environ["STRIPE"] except KeyError: STRIPE_TOKEN = "" - print("Starting without a STRIPE Token will not allow you to accept Donations!") + warning("Starting without a STRIPE Token will not allow you to accept Donations!") s = Router() t = T_info() @@ -47,11 +49,12 @@ logging.basicConfig( ) logger = logging.getLogger(__name__) -print("Bot Online") +info("Bot script started.") def start(update: Update, context: CallbackContext): - """Send a message when the command /start is issued.""" + """Send help text when the command /start is issued.""" + info(f"Start command ran by {update.message.chat.username}") update.message.reply_text( text=t.help_text, parse_mode=telegram.ParseMode.MARKDOWN, @@ -60,7 +63,8 @@ def start(update: Update, context: CallbackContext): def help(update: Update, context: CallbackContext): - """Send link to docs when the command /help is issued.""" + """Send help text when the command /help is issued.""" + info(f"Help command ran by {update.message.chat.username}") update.message.reply_text( text=t.help_text, parse_mode=telegram.ParseMode.MARKDOWN, @@ -69,7 +73,8 @@ def help(update: Update, context: CallbackContext): def license(update: Update, context: CallbackContext): - """Return bots license agreement""" + """Send bots license when the /license command is issued.""" + info(f"License command ran by {update.message.chat.username}") update.message.reply_text( text=t.license, parse_mode=telegram.ParseMode.MARKDOWN, @@ -78,6 +83,8 @@ def license(update: Update, context: CallbackContext): def status(update: Update, context: CallbackContext): + """Gather status of bot and dependant services and return important status updates.""" + warning(f"Status command ran by {update.message.chat.username}") bot_resp = datetime.datetime.now(update.message.date.tzinfo) - update.message.date update.message.reply_text( @@ -90,6 +97,8 @@ def status(update: Update, context: CallbackContext): def donate(update: Update, context: CallbackContext): + """Sets up donation.""" + info(f"Donate command ran by {update.message.chat.username}") chat_id = update.message.chat_id if update.message.text.strip() == "/donate": @@ -107,7 +116,7 @@ def donate(update: Update, context: CallbackContext): except ValueError: update.message.reply_text(f"{amount} is not a valid donation amount or number.") return - print(price) + info(f"Donation amount: {price}") context.bot.send_invoice( chat_id=chat_id, @@ -119,13 +128,15 @@ def donate(update: Update, context: CallbackContext): 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_url="https://simple-stock-bots.gitlab.io/docs/img/Telegram.png", photo_width=500, photo_height=500, ) def precheckout_callback(update: Update, context: CallbackContext): + """Approves donation""" + info(f"precheckout_callback queried") query = update.pre_checkout_query query.answer(ok=True) @@ -138,6 +149,8 @@ def precheckout_callback(update: Update, context: CallbackContext): def successful_payment_callback(update: Update, context: CallbackContext): + """Thanks user for donation""" + info(f"Successful payment!") update.message.reply_text( "Thank you for your donation! It goes a long way to keeping the bot free!" ) @@ -148,6 +161,7 @@ def symbol_detect(update: Update, context: CallbackContext): Runs on any message that doesn't have a command and searches for symbols, then returns the prices of any symbols found. """ + message = update.message.text chat_id = update.message.chat_id symbols = s.find_symbols(message) @@ -155,7 +169,9 @@ 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) - print(symbols) + info(f"User called for symbols: {update.message.chat.username}") + info(f"Symbols found: {symbols}") + for reply in s.price_reply(symbols): update.message.reply_text( text=reply, @@ -165,9 +181,8 @@ def symbol_detect(update: Update, context: CallbackContext): def dividend(update: Update, context: CallbackContext): - """ - waits for /dividend or /div command and then finds dividend info on that symbol. - """ + """/dividend or /div command and then finds dividend info on that symbol.""" + info(f"Dividend command ran by {update.message.chat.username}") message = update.message.text chat_id = update.message.chat_id @@ -190,9 +205,8 @@ def dividend(update: Update, context: CallbackContext): def news(update: Update, context: CallbackContext): - """ - waits for /news command and then finds news info on that symbol. - """ + """/news command then finds news info on that symbol.""" + info(f"News command ran by {update.message.chat.username}") message = update.message.text chat_id = update.message.chat_id @@ -215,10 +229,9 @@ def news(update: Update, context: CallbackContext): ) -def info(update: Update, context: CallbackContext): - """ - waits for /info command and then finds info on that symbol. - """ +def information(update: Update, context: CallbackContext): + """/info command then finds info on that symbol.""" + info(f"Information command ran by {update.message.chat.username}") message = update.message.text chat_id = update.message.chat_id @@ -242,6 +255,11 @@ def info(update: Update, context: CallbackContext): def search(update: Update, context: CallbackContext): + """ + Uses fuzzy search on full list of stocks and crypto names + and descriptions then returns the top matches in order. + """ + info(f"Search command ran by {update.message.chat.username}") message = update.message.text.replace("/search ", "") chat_id = update.message.chat_id @@ -265,7 +283,8 @@ def search(update: Update, context: CallbackContext): def intra(update: Update, context: CallbackContext): - # TODO: Document usage of this command. https://iexcloud.io/docs/api/#historical-prices + """returns a chart of intraday data for a symbol""" + info(f"Intra command ran by {update.message.chat.username}") message = update.message.text chat_id = update.message.chat_id @@ -320,7 +339,8 @@ def intra(update: Update, context: CallbackContext): def chart(update: Update, context: CallbackContext): - # TODO: Document usage of this command. https://iexcloud.io/docs/api/#historical-prices + """returns a chart of the past month of data for a symbol""" + info(f"Chart command ran by {update.message.chat.username}") message = update.message.text chat_id = update.message.chat_id @@ -350,7 +370,7 @@ 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, @@ -372,9 +392,8 @@ def chart(update: Update, context: CallbackContext): def stat(update: Update, context: CallbackContext): - """ - https://iexcloud.io/docs/api/#key-stats - """ + """returns key statistics on symbol""" + info(f"Stat command ran by {update.message.chat.username}") message = update.message.text chat_id = update.message.chat_id @@ -397,10 +416,34 @@ def stat(update: Update, context: CallbackContext): ) +def cap(update: Update, context: CallbackContext): + """returns market cap for symbol""" + info(f"Cap command ran by {update.message.chat.username}") + message = update.message.text + chat_id = update.message.chat_id + + if message.strip().split("@")[0] == "/cap": + update.message.reply_text( + "This command returns the market cap for a symbol.\nExample: /cap $tsla" + ) + return + + symbols = s.find_symbols(message) + + if symbols: + context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) + + for reply in s.cap_reply(symbols): + update.message.reply_text( + text=reply, + parse_mode=telegram.ParseMode.MARKDOWN, + disable_notification=True, + ) + + def trending(update: Update, context: CallbackContext): - """ - Trending Symbols - """ + """returns currently trending symbols and how much they've moved in the past trading day.""" + info(f"Trending command ran by {update.message.chat.username}") chat_id = update.message.chat_id @@ -415,17 +458,17 @@ def trending(update: Update, context: CallbackContext): def inline_query(update: Update, context: CallbackContext): """ - Handles inline query. - Does a fuzzy search on input and returns stocks that are close. + Handles inline query. Searches by looking if query is contained + in the symbol and returns matches in alphabetical order. """ - + info(f"Inline command ran by {update.message.chat.username}") + info(f"Query: {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)) results = [] - print(update.inline_query.query) for match, price in zip(matches, prices): try: results.append( @@ -438,16 +481,21 @@ def inline_query(update: Update, context: CallbackContext): ) ) except TypeError: - logging.warning(str(match)) + warning(f"{match} caused error in inline query.") pass - print(match[0], "\n\n\n") + if len(results) == 5: update.inline_query.answer(results) + info("Inline Command was successful") return update.inline_query.answer(results) def rand_pick(update: Update, context: CallbackContext): + """For the gamblers. Returns a random symbol to buy and a sell date""" + info( + f"Someone is gambling! Random_pick command ran by {update.message.chat.username}" + ) update.message.reply_text( text=s.random_pick(), @@ -458,24 +506,32 @@ def rand_pick(update: Update, context: CallbackContext): def error(update: Update, context: CallbackContext): """Log Errors caused by Updates.""" - logger.warning('Update "%s" caused error "%s"', update, error) + warning('Update "%s" caused error "%s"', update, error) tb_list = traceback.format_exception( 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)}
" - # ) + + err_code = "".join([random.choice(string.ascii_lowercase) for i in range(5)]) + warning(f"Logging error: {err_code}") + + 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)}
" + ) + warning(message) + else: + warning(tb_string) + update.message.reply_text( - text="An error has occured. Please inform @MisterBiggs if the error persists." + text=f"An error has occured. Please inform @MisterBiggs if the error persists. Error Code: `{err_code}`", + parse_mode=telegram.ParseMode.MARKDOWN, ) # Finally, send the message @@ -498,18 +554,23 @@ def main(): dp.add_handler(CommandHandler("dividend", dividend)) dp.add_handler(CommandHandler("div", dividend)) dp.add_handler(CommandHandler("news", news)) - dp.add_handler(CommandHandler("info", info)) + dp.add_handler(CommandHandler("info", information)) dp.add_handler(CommandHandler("stat", stat)) dp.add_handler(CommandHandler("stats", stat)) + dp.add_handler(CommandHandler("cap", cap)) dp.add_handler(CommandHandler("trending", trending)) 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, run_async=True)) dp.add_handler(CommandHandler("random", rand_pick)) dp.add_handler(CommandHandler("donate", donate)) dp.add_handler(CommandHandler("status", status)) + # Charting can be slow so they run async. + dp.add_handler(CommandHandler("intra", intra, run_async=True)) + dp.add_handler(CommandHandler("intraday", intra, run_async=True)) + dp.add_handler(CommandHandler("day", intra, run_async=True)) + dp.add_handler(CommandHandler("chart", chart, run_async=True)) + dp.add_handler(CommandHandler("month", chart, run_async=True)) + # on noncommand i.e message - echo the message on Telegram dp.add_handler(MessageHandler(Filters.text, symbol_detect)) diff --git a/cg_Crypto.py b/cg_Crypto.py index 6413040..00f248c 100644 --- a/cg_Crypto.py +++ b/cg_Crypto.py @@ -2,13 +2,14 @@ """ from datetime import datetime -from typing import Optional, List, Tuple +from typing import List, Optional, Tuple import pandas as pd import requests as r import schedule from fuzzywuzzy import fuzz from markdownify import markdownify + from Symbol import Coin @@ -117,7 +118,7 @@ class cg_Crypto: self.searched_symbols[search] = symbol_list return symbol_list - def price_reply(self, symbol: Coin) -> str: + def price_reply(self, coin: Coin) -> str: """Returns current market price or after hours if its available for a given coin symbol. Parameters @@ -133,22 +134,22 @@ 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/simple/price?ids={coin.id}&vs_currencies={self.vs_currency}&include_24hr_change=true", timeout=5, ) if response.status_code == 200: - data = response.json() try: - name = data["name"] - price = data["market_data"]["current_price"][self.vs_currency] - change = data["market_data"]["price_change_percentage_24h"] + data = response.json()[coin.id] + + price = data[self.vs_currency] + change = data[self.vs_currency + "_24h_change"] if change is None: change = 0 except KeyError: - return f"{symbol} returned an error." + return f"{coin.id} returned an error." - message = f"The current price of {name} is $**{price:,}**" + message = f"The current price of {coin.name} is $**{price:,}**" # Determine wording of change text if change > 0: @@ -159,7 +160,7 @@ class cg_Crypto: message += ", the coin hasn't shown any movement today." else: - message = f"The Coin: {symbol.name} was not found." + message = f"The Coin: {coin.name} was not found." return message @@ -220,18 +221,18 @@ class cg_Crypto: return pd.DataFrame() def stat_reply(self, symbol: Coin) -> str: - """Gets key statistics for each symbol in the list + """Gathers key statistics on coin. Mostly just CoinGecko scores. Parameters ---------- - symbols : List[str] - List of coin symbols + symbol : Coin Returns ------- - Dict[str, str] - Each symbol passed in is a key with its value being a human readable formatted string of the symbols statistics. + str + Preformatted markdown. """ + response = r.get( f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false", timeout=5, @@ -252,18 +253,53 @@ class cg_Crypto: else: return f"{symbol.symbol} returned an error." - def info_reply(self, symbol: Coin) -> str: - """Gets information on stock symbols. + def cap_reply(self, coin: Coin) -> str: + """Gets market cap for Coin Parameters ---------- - symbols : List[str] - List of stock symbols. + coin : Coin Returns ------- - Dict[str, str] - Each symbol passed in is a key with its value being a human readable formatted string of the symbols information. + str + Preformatted markdown. + """ + response = r.get( + f"https://api.coingecko.com/api/v3/simple/price?ids={coin.id}&vs_currencies={self.vs_currency}&include_market_cap=true", + timeout=5, + ) + if response.status_code == 200: + + try: + data = response.json()[coin.id] + + price = data[self.vs_currency] + cap = data[self.vs_currency + "_market_cap"] + except KeyError: + return f"{coin.id} returned an error." + + if cap == 0: + return f"The market cap for {coin.name} is not available for unknown reasons." + + message = f"The current price of {coin.name} is $**{price:,}** and its market cap is $**{cap:,.2f}** {self.vs_currency.upper()}" + + else: + message = f"The Coin: {coin.name} was not found or returned and error." + + return message + + def info_reply(self, symbol: Coin) -> str: + """Gets coin description + + Parameters + ---------- + symbol : Coin + + Returns + ------- + str + Preformatted markdown. """ response = r.get( @@ -285,17 +321,47 @@ class cg_Crypto: Returns ------- list[str] - list of $$ID: NAME + list of $$ID: NAME, CHANGE% """ coins = r.get( "https://api.coingecko.com/api/v3/search/trending", timeout=5, - ).json()["coins"] + ) + try: + trending = [] + if coins.status_code == 200: + for coin in coins.json()["coins"]: + c = coin["item"] - return [f"$${c['item']['symbol'].upper()}: {c['item']['name']}" for c in coins] + sym = c["symbol"].upper() + name = c["name"] + change = r.get( + f"https://api.coingecko.com/api/v3/simple/price?ids={c['id']}&vs_currencies={self.vs_currency}&include_24hr_change=true" + ).json()[c["id"]]["usd_24h_change"] + + msg = f"`$${sym}`: {name}, {change:.2f}%" + + trending.append(msg) + + except Exception as e: + print(e) + trending = ["Trending Coins Currently Unavailable."] + + return trending def batch_price(self, coins: list[Coin]) -> list[str]: + """Gets price of a list of coins all in one API call + + Parameters + ---------- + coins : list[Coin] + + Returns + ------- + list[str] + returns preformatted list of strings detailing price movement of each coin passed in. + """ query = ",".join([c.id for c in coins]) prices = r.get( diff --git a/search_index.pickle b/search_index.pickle deleted file mode 100644 index 26e0883..0000000 Binary files a/search_index.pickle and /dev/null differ diff --git a/searchindexer.ipynb b/searchindexer.ipynb deleted file mode 100644 index 66d422e..0000000 --- a/searchindexer.ipynb +++ /dev/null @@ -1,276 +0,0 @@ -{ - "metadata": { - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.0-final" - }, - "orig_nbformat": 2, - "kernelspec": { - "name": "python3", - "display_name": "Python 3.9.0 64-bit", - "metadata": { - "interpreter": { - "hash": "36cf16204b8548560b1c020c4e8fb5b57f0e4c58016f52f2d4be01e192833930" - } - } - } - }, - "nbformat": 4, - "nbformat_minor": 2, - "cells": [ - { - "cell_type": "code", - "execution_count": 62, - "metadata": {}, - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Requirement already satisfied: tqdm in /home/anson/.local/lib/python3.8/site-packages (4.59.0)\n" - ] - } - ], - "source": [ - "!pip install tqdm" - ] - }, - { - "cell_type": "code", - "execution_count": 64, - "metadata": {}, - "outputs": [], - "source": [ - "import requests as r\n", - "import pandas as pd\n", - "from fuzzywuzzy import fuzz\n", - "from functools import cache\n", - "from tqdm import tqdm" - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "metadata": {}, - "outputs": [], - "source": [ - " def stocks():\n", - "\n", - " raw_symbols = r.get(\n", - " f\"https://cloud.iexapis.com/stable/ref-data/symbols?token=WOOOPS\"\n", - " ).json()\n", - " symbols = pd.DataFrame(data=raw_symbols)\n", - "\n", - " symbols[\"description\"] = \"$\" + symbols[\"symbol\"] + \": \" + symbols[\"name\"]\n", - " symbols[\"id\"] = symbols[\"symbol\"]\n", - "\n", - " symbols = symbols[[\"id\", \"symbol\", \"name\", \"description\"]]\n", - "\n", - " return symbols\n", - "\n", - "\n", - "\n", - " def coins():\n", - "\n", - " raw_symbols = r.get(\"https://api.coingecko.com/api/v3/coins/list\").json()\n", - " symbols = pd.DataFrame(data=raw_symbols)\n", - "\n", - " symbols[\"description\"] = \"$$\" + symbols[\"symbol\"] + \": \" + symbols[\"name\"]\n", - " symbols = symbols[[\"id\", \"symbol\", \"name\", \"description\"]]\n", - "\n", - " return symbols" - ] - }, - { - "cell_type": "code", - "execution_count": 51, - "metadata": {}, - "outputs": [ - { - "output_type": "execute_result", - "data": { - "text/plain": [ - " id symbol \\\n", - "0 A A \n", - "1 AA AA \n", - "2 AAA AAA \n", - "3 AAAU AAAU \n", - "4 AAC AAC \n", - "... ... ... \n", - "6565 zyro zyro \n", - "6566 zytara-dollar zusd \n", - "6567 zyx zyx \n", - "6568 zzz-finance zzz \n", - "6569 zzz-finance-v2 zzzv2 \n", - "\n", - " name \\\n", - "0 Agilent Technologies Inc. \n", - "1 Alcoa Corp \n", - "2 Listed Funds Trust - AAF First Priority CLO Bo... \n", - "3 Goldman Sachs Physical Gold ETF Shares - Goldm... \n", - "4 Ares Acquisition Corporation - Class A \n", - "... ... \n", - "6565 Zyro \n", - "6566 Zytara Dollar \n", - "6567 ZYX \n", - "6568 zzz.finance \n", - "6569 zzz.finance v2 \n", - "\n", - " description \n", - "0 $A: Agilent Technologies Inc. \n", - "1 $AA: Alcoa Corp \n", - "2 $AAA: Listed Funds Trust - AAF First Priority ... \n", - "3 $AAAU: Goldman Sachs Physical Gold ETF Shares ... \n", - "4 $AAC: Ares Acquisition Corporation - Class A \n", - "... ... \n", - "6565 $$zyro: Zyro \n", - "6566 $$zusd: Zytara Dollar \n", - "6567 $$zyx: ZYX \n", - "6568 $$zzz: zzz.finance \n", - "6569 $$zzzv2: zzz.finance v2 \n", - "\n", - "[16946 rows x 4 columns]" - ], - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
idsymbolnamedescription
0AAAgilent Technologies Inc.$A: Agilent Technologies Inc.
1AAAAAlcoa Corp$AA: Alcoa Corp
2AAAAAAListed Funds Trust - AAF First Priority CLO Bo...$AAA: Listed Funds Trust - AAF First Priority ...
3AAAUAAAUGoldman Sachs Physical Gold ETF Shares - Goldm...$AAAU: Goldman Sachs Physical Gold ETF Shares ...
4AACAACAres Acquisition Corporation - Class A$AAC: Ares Acquisition Corporation - Class A
...............
6565zyrozyroZyro$$zyro: Zyro
6566zytara-dollarzusdZytara Dollar$$zusd: Zytara Dollar
6567zyxzyxZYX$$zyx: ZYX
6568zzz-financezzzzzz.finance$$zzz: zzz.finance
6569zzz-finance-v2zzzv2zzz.finance v2$$zzzv2: zzz.finance v2
\n

16946 rows × 4 columns

\n
" - }, - "metadata": {}, - "execution_count": 51 - } - ], - "source": [ - "df = pd.concat([stocks(), coins()])\n", - "df" - ] - }, - { - "cell_type": "code", - "execution_count": 79, - "metadata": {}, - "outputs": [], - "source": [ - " def search_symbols(search: str):\n", - " \"\"\"Performs a fuzzy search to find stock symbols closest to a search term.\n", - "\n", - " Parameters\n", - " ----------\n", - " search : str\n", - " String used to search, could be a company name or something close to the companies stock ticker.\n", - "\n", - " Returns\n", - " -------\n", - " List[tuple[str, str]]\n", - " A list tuples of every stock sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name).\n", - " \"\"\"\n", - "\n", - " try:\n", - " if search_index[search]: return search_index[search]\n", - " except KeyError:\n", - " pass\n", - "\n", - "\n", - "\n", - " search = search.lower()\n", - "\n", - " df[\"Match\"] = df.apply(\n", - " lambda x: fuzz.ratio(search, f\"{x['symbol']}\".lower()),\n", - " axis=1,\n", - " )\n", - "\n", - " df.sort_values(by=\"Match\", ascending=False, inplace=True)\n", - " if df[\"Match\"].head().sum() < 300:\n", - " df[\"Match\"] = df.apply(\n", - " lambda x: fuzz.partial_ratio(search, x[\"name\"].lower()),\n", - " axis=1,\n", - " )\n", - "\n", - " df.sort_values(by=\"Match\", ascending=False, inplace=True)\n", - "\n", - " symbols = df.head(20)\n", - " symbol_list = list(zip(list(symbols[\"symbol\"]), list(symbols[\"description\"])))\n", - " \n", - " return symbol_list" - ] - }, - { - "cell_type": "code", - "execution_count": 91, - "metadata": {}, - "outputs": [], - "source": [ - "search_list = df['id'].to_list() + df['description'].to_list()\n", - "search_index = {}" - ] - }, - { - "cell_type": "code", - "execution_count": 92, - "metadata": { - "tags": [] - }, - "outputs": [ - { - "output_type": "stream", - "name": "stderr", - "text": [ - " 5%|▍ | 1545/33892 [06:51<2:23:40, 3.75it/s]\n" - ] - }, - { - "output_type": "error", - "ename": "KeyboardInterrupt", - "evalue": "", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mi\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mtqdm\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msearch_list\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0msearch_index\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msearch_symbols\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;32m\u001b[0m in \u001b[0;36msearch_symbols\u001b[0;34m(search)\u001b[0m\n\u001b[1;32m 22\u001b[0m \u001b[0msearch\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msearch\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlower\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 24\u001b[0;31m df[\"Match\"] = df.apply(\n\u001b[0m\u001b[1;32m 25\u001b[0m \u001b[0;32mlambda\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mfuzz\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mratio\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msearch\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34mf\"{x['symbol']}\"\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlower\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 26\u001b[0m \u001b[0maxis\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/.local/lib/python3.9/site-packages/pandas/core/frame.py\u001b[0m in \u001b[0;36mapply\u001b[0;34m(self, func, axis, raw, result_type, args, **kwds)\u001b[0m\n\u001b[1;32m 7763\u001b[0m \u001b[0mkwds\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mkwds\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 7764\u001b[0m )\n\u001b[0;32m-> 7765\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mop\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_result\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 7766\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 7767\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mapplymap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfunc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mna_action\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0mDataFrame\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/.local/lib/python3.9/site-packages/pandas/core/apply.py\u001b[0m in \u001b[0;36mget_result\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 183\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mapply_raw\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 184\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 185\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mapply_standard\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 186\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 187\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mapply_empty_result\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/.local/lib/python3.9/site-packages/pandas/core/apply.py\u001b[0m in \u001b[0;36mapply_standard\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 274\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 275\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mapply_standard\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 276\u001b[0;31m \u001b[0mresults\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mres_index\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mapply_series_generator\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 277\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 278\u001b[0m \u001b[0;31m# wrap results\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/.local/lib/python3.9/site-packages/pandas/core/apply.py\u001b[0m in \u001b[0;36mapply_series_generator\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 286\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 287\u001b[0m \u001b[0;32mwith\u001b[0m \u001b[0moption_context\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"mode.chained_assignment\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 288\u001b[0;31m \u001b[0;32mfor\u001b[0m \u001b[0mi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mv\u001b[0m \u001b[0;32min\u001b[0m \u001b[0menumerate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mseries_gen\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 289\u001b[0m \u001b[0;31m# ignore SettingWithCopy here in case the user mutates\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 290\u001b[0m \u001b[0mresults\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mf\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mv\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/.local/lib/python3.9/site-packages/pandas/core/apply.py\u001b[0m in \u001b[0;36mseries_generator\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 408\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0marr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mname\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mzip\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvalues\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mindex\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 409\u001b[0m \u001b[0;31m# GH#35462 re-pin mgr in case setitem changed it\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 410\u001b[0;31m \u001b[0mser\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_mgr\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmgr\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 411\u001b[0m \u001b[0mblk\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalues\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0marr\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 412\u001b[0m \u001b[0mser\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mname\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mname\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/.local/lib/python3.9/site-packages/pandas/core/generic.py\u001b[0m in \u001b[0;36m__setattr__\u001b[0;34m(self, name, value)\u001b[0m\n\u001b[1;32m 5473\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5474\u001b[0m \u001b[0mobject\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__getattribute__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mname\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 5475\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mobject\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__setattr__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mname\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 5476\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mAttributeError\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5477\u001b[0m \u001b[0;32mpass\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mKeyboardInterrupt\u001b[0m: " - ] - } - ], - "source": [ - "\n", - "for i in tqdm(search_list):\n", - " search_index[i] = search_symbols(i)" - ] - }, - { - "cell_type": "code", - "execution_count": 89, - "metadata": {}, - "outputs": [], - "source": [ - "import pickle\n", - "\n", - "\n", - "\n", - "with open('search_index.pickle', 'wb') as handle:\n", - " pickle.dump(search_index, handle, protocol=pickle.HIGHEST_PROTOCOL)\n", - "\n", - "# with open('filename.pickle', 'rb') as handle:\n", - "# b = pickle.load(handle)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ] -} diff --git a/symbol_router.py b/symbol_router.py index 8da9a44..efcecd4 100644 --- a/symbol_router.py +++ b/symbol_router.py @@ -1,18 +1,17 @@ """Function that routes symbols to the correct API provider. """ -import re -import pandas as pd -import random import datetime +import random +import re +from logging import critical, debug, error, info, warning + +import pandas as pd from fuzzywuzzy import fuzz -from typing import List, Tuple - -from IEX_Symbol import IEX_Symbol from cg_Crypto import cg_Crypto - -from Symbol import Symbol, Stock, Coin +from IEX_Symbol import IEX_Symbol +from Symbol import Coin, Stock, Symbol class Router: @@ -24,10 +23,9 @@ class Router: self.stock = IEX_Symbol() self.crypto = cg_Crypto() - 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 in a blob of text and returns them in a list. - Only returns each match once. Example: Whats the price of $tsla? Parameters ---------- @@ -36,7 +34,7 @@ class Router: Returns ------- - List[str] + list[str] List of stock symbols as strings without dollar sign. """ symbols = [] @@ -45,15 +43,15 @@ class Router: if stock.upper() in self.stock.symbol_list["symbol"].values: symbols.append(Stock(stock)) else: - print(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)) for coin in coins: if coin.lower() in self.crypto.symbol_list["symbol"].values: symbols.append(Coin(coin.lower())) else: - print(f"{coin} is not in list of coins") - print(symbols) + info(f"{coin} is not in list of coins") + info(symbols) return symbols def status(self, bot_resp) -> str: @@ -65,7 +63,7 @@ class Router: Human readable text on status of the bot and relevant APIs """ - return f""" + stats = f""" Bot Status: {bot_resp} @@ -76,7 +74,11 @@ class Router: {self.crypto.status()} """ - def search_symbols(self, search: str) -> List[Tuple[str, str]]: + warning(stats) + + return stats + + def search_symbols(self, search: str) -> list[tuple[str, str]]: """Performs a fuzzy search to find stock symbols closest to a search term. Parameters @@ -86,8 +88,9 @@ class Router: 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). + list[tuple[str, str]] + A list tuples of every stock sorted in order of how well they match. + Each tuple contains: (Symbol, Issue Name). """ df = pd.concat([self.stock.symbol_list, self.crypto.symbol_list]) @@ -113,7 +116,7 @@ class Router: self.searched_symbols[search] = 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. Should be very fast compared to a fuzzy search. @@ -124,7 +127,7 @@ class Router: Returns ------- - List[tuple[str, str]] + list[tuple[str, str]] Each tuple contains: (Symbol, Issue Name). """ @@ -141,7 +144,7 @@ class Router: self.searched_symbols[search] = 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. Parameters @@ -158,17 +161,17 @@ class Router: replies = [] for symbol in symbols: - print(symbol) + info(symbol) 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") + info(f"{symbol} is not a Stock or Coin") 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. Parameters @@ -179,7 +182,8 @@ class Router: 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. + 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: @@ -192,7 +196,7 @@ class Router: 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. Parameters @@ -203,7 +207,8 @@ class Router: 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. + Each symbol passed in is a key with its value being a human + readable markdown formatted string of the symbols news. """ replies = [] @@ -220,18 +225,19 @@ class Router: return replies - def info_reply(self, symbols: list) -> List[str]: + def info_reply(self, symbols: list) -> list[str]: """Gets information on stock symbols. Parameters ---------- - symbols : List[str] + 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. + Each symbol passed in is a key with its value being a human readable formatted + string of the symbols information. """ replies = [] @@ -256,7 +262,8 @@ class Router: Returns ------- pd.DataFrame - Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. + Returns a timeseries dataframe with high, low, and volume data if its available. + Otherwise returns empty pd.DataFrame. """ if isinstance(symbol, Stock): @@ -279,7 +286,8 @@ class Router: Returns ------- pd.DataFrame - Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. + Returns a timeseries dataframe with high, low, and volume data if its available. + Otherwise returns empty pd.DataFrame. """ if isinstance(symbol, Stock): return self.stock.chart_reply(symbol) @@ -289,18 +297,19 @@ class Router: print(f"{symbol} is not a Stock or Coin") 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 Parameters ---------- - symbols : List[str] + 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. + Each symbol passed in is a key with its value being a human readable + formatted string of the symbols statistics. """ replies = [] @@ -314,6 +323,32 @@ class Router: return replies + def cap_reply(self, symbols: list[Symbol]) -> list[str]: + """Gets market cap 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 market cap. + """ + replies = [] + + for symbol in symbols: + if isinstance(symbol, Stock): + replies.append(self.stock.cap_reply(symbol)) + elif isinstance(symbol, Coin): + replies.append(self.crypto.cap_reply(symbol)) + else: + print(f"{symbol} is not a Stock or Coin") + + return replies + def trending(self) -> str: """Checks APIs for trending symbols. @@ -326,7 +361,7 @@ class Router: stocks = self.stock.trending() coins = self.crypto.trending() - reply = "`Trending Stocks:\n" + reply = "Trending Stocks:\n" reply += "-" * len("Trending Stocks:") + "\n" for stock in stocks: reply += stock + "\n" @@ -336,7 +371,7 @@ class Router: for coin in coins: reply += coin + "\n" - return reply + "`" + return reply def random_pick(self) -> str: @@ -350,7 +385,7 @@ class Router: 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. Parameters