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

Merge branch 'canary' into 'master'

Canary

Closes #68, #70, and #69

See merge request simple-stock-bots/simple-telegram-stock-bot!24
This commit is contained in:
Anson Biggs 2021-07-07 19:13:01 +00:00
commit 24220cd0cc
8 changed files with 364 additions and 433 deletions

View File

@ -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."]

View File

@ -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

View File

@ -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

169
bot.py
View File

@ -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"<pre>update = {html.escape(json.dumps(update.to_dict(), indent=2, ensure_ascii=False))}"
# "</pre>\n\n"
# f"<pre>context.chat_data = {html.escape(str(context.chat_data))}</pre>\n\n"
# f"<pre>context.user_data = {html.escape(str(context.user_data))}</pre>\n\n"
# f"<pre>{html.escape(tb_string)}</pre>"
# )
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"<pre>update = {html.escape(json.dumps(update.to_dict(), indent=2, ensure_ascii=False))}"
"</pre>\n\n"
f"<pre>context.chat_data = {html.escape(str(context.chat_data))}</pre>\n\n"
f"<pre>context.user_data = {html.escape(str(context.user_data))}</pre>\n\n"
f"<pre>{html.escape(tb_string)}</pre>"
)
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))

View File

@ -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(

Binary file not shown.

View File

@ -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": "<div>\n<style scoped>\n .dataframe tbody tr th:only-of-type {\n vertical-align: middle;\n }\n\n .dataframe tbody tr th {\n vertical-align: top;\n }\n\n .dataframe thead th {\n text-align: right;\n }\n</style>\n<table border=\"1\" class=\"dataframe\">\n <thead>\n <tr style=\"text-align: right;\">\n <th></th>\n <th>id</th>\n <th>symbol</th>\n <th>name</th>\n <th>description</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <th>0</th>\n <td>A</td>\n <td>A</td>\n <td>Agilent Technologies Inc.</td>\n <td>$A: Agilent Technologies Inc.</td>\n </tr>\n <tr>\n <th>1</th>\n <td>AA</td>\n <td>AA</td>\n <td>Alcoa Corp</td>\n <td>$AA: Alcoa Corp</td>\n </tr>\n <tr>\n <th>2</th>\n <td>AAA</td>\n <td>AAA</td>\n <td>Listed Funds Trust - AAF First Priority CLO Bo...</td>\n <td>$AAA: Listed Funds Trust - AAF First Priority ...</td>\n </tr>\n <tr>\n <th>3</th>\n <td>AAAU</td>\n <td>AAAU</td>\n <td>Goldman Sachs Physical Gold ETF Shares - Goldm...</td>\n <td>$AAAU: Goldman Sachs Physical Gold ETF Shares ...</td>\n </tr>\n <tr>\n <th>4</th>\n <td>AAC</td>\n <td>AAC</td>\n <td>Ares Acquisition Corporation - Class A</td>\n <td>$AAC: Ares Acquisition Corporation - Class A</td>\n </tr>\n <tr>\n <th>...</th>\n <td>...</td>\n <td>...</td>\n <td>...</td>\n <td>...</td>\n </tr>\n <tr>\n <th>6565</th>\n <td>zyro</td>\n <td>zyro</td>\n <td>Zyro</td>\n <td>$$zyro: Zyro</td>\n </tr>\n <tr>\n <th>6566</th>\n <td>zytara-dollar</td>\n <td>zusd</td>\n <td>Zytara Dollar</td>\n <td>$$zusd: Zytara Dollar</td>\n </tr>\n <tr>\n <th>6567</th>\n <td>zyx</td>\n <td>zyx</td>\n <td>ZYX</td>\n <td>$$zyx: ZYX</td>\n </tr>\n <tr>\n <th>6568</th>\n <td>zzz-finance</td>\n <td>zzz</td>\n <td>zzz.finance</td>\n <td>$$zzz: zzz.finance</td>\n </tr>\n <tr>\n <th>6569</th>\n <td>zzz-finance-v2</td>\n <td>zzzv2</td>\n <td>zzz.finance v2</td>\n <td>$$zzzv2: zzz.finance v2</td>\n </tr>\n </tbody>\n</table>\n<p>16946 rows × 4 columns</p>\n</div>"
},
"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<ipython-input-92-559f57361529>\u001b[0m in \u001b[0;36m<module>\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<ipython-input-79-2cfb15c9428a>\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": []
}
]
}

View File

@ -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