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 'crypto'

This commit is contained in:
Anson Biggs 2021-03-28 13:38:40 -07:00
commit 042e50cc26
10 changed files with 1151 additions and 658 deletions

View File

@ -1,4 +1,4 @@
FROM python:3.8-buster
FROM python:3.9-buster

423
IEX_Symbol.py Normal file
View File

@ -0,0 +1,423 @@
"""Class with functions for running the bot with IEX Cloud.
"""
from datetime import datetime
from typing import Optional, List, Tuple
import pandas as pd
import requests as r
import schedule
from fuzzywuzzy import fuzz
import os
from Symbol import Stock
class IEX_Symbol:
"""
Functions for finding stock market information about symbols.
"""
SYMBOL_REGEX = "[$]([a-zA-Z]{1,4})"
searched_symbols = {}
charts = {}
def __init__(self) -> None:
"""Creates a Symbol Object
Parameters
----------
IEX_TOKEN : str
IEX Token
"""
try:
self.IEX_TOKEN = os.environ["IEX"]
except KeyError:
self.IEX_TOKEN = ""
print(
"Starting without an IEX Token will not allow you to get market data!"
)
if self.IEX_TOKEN != "":
self.get_symbol_list()
schedule.every().day.do(self.get_symbol_list)
schedule.every().day.do(self.clear_charts)
def clear_charts(self) -> None:
"""Clears cache of chart data."""
self.charts = {}
def get_symbol_list(
self, return_df=False
) -> Optional[Tuple[pd.DataFrame, datetime]]:
raw_symbols = r.get(
f"https://cloud.iexapis.com/stable/ref-data/symbols?token={self.IEX_TOKEN}"
).json()
symbols = pd.DataFrame(data=raw_symbols)
symbols["description"] = symbols["symbol"] + ": " + symbols["name"]
self.symbol_list = symbols
if return_df:
return symbols, datetime.now()
def status(self) -> str:
"""Checks IEX Status dashboard for any current API issues.
Returns
-------
str
Human readable text on status of IEX API
"""
resp = r.get("https://pjmps0c34hp7.statuspage.io/api/v2/status.json")
if resp.status_code == 200:
status = resp.json()["status"]
else:
return "IEX Cloud did not respond. Please check their status page for more information. https://status.iexapis.com"
if status["indicator"] == "none":
return "IEX Cloud is currently not reporting any issues with its API."
else:
return (
f"{status['indicator']}: {status['description']}."
+ " Please check the status page for more information. https://status.iexapis.com"
)
def search_symbols(self, search: str) -> List[Tuple[str, str]]:
"""Performs a fuzzy search to find stock symbols closest to a search term.
Parameters
----------
search : str
String used to search, could be a company name or something close to the companies stock ticker.
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).
"""
schedule.run_pending()
search = search.lower()
try: # https://stackoverflow.com/a/3845776/8774114
return self.searched_symbols[search]
except KeyError:
pass
symbols = self.symbol_list
symbols["Match"] = symbols.apply(
lambda x: fuzz.ratio(search, f"{x['symbol']}".lower()),
axis=1,
)
symbols.sort_values(by="Match", ascending=False, inplace=True)
if symbols["Match"].head().sum() < 300:
symbols["Match"] = symbols.apply(
lambda x: fuzz.partial_ratio(search, x["name"].lower()),
axis=1,
)
symbols.sort_values(by="Match", ascending=False, inplace=True)
symbols = symbols.head(10)
symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"])))
self.searched_symbols[search] = symbol_list
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.
Parameters
----------
symbols : list
List of stock symbols.
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.
"""
IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol.id}/quote?token={self.IEX_TOKEN}"
response = r.get(IEXurl)
if response.status_code == 200:
IEXData = response.json()
keys = (
"isUSMarketOpen",
"extendedChangePercent",
"extendedPrice",
"companyName",
"latestPrice",
"changePercent",
)
if set(keys).issubset(IEXData):
try: # Some symbols dont return if the market is open
IEXData["isUSMarketOpen"]
except KeyError:
IEXData["isUSMarketOpen"] = True
if (
IEXData["isUSMarketOpen"]
or (IEXData["extendedChangePercent"] is None)
or (IEXData["extendedPrice"] is None)
): # Check if market is open.
message = f"The current stock price of {IEXData['companyName']} is $**{IEXData['latestPrice']}**"
change = round(IEXData["changePercent"] * 100, 2)
else:
message = (
f"{IEXData['companyName']} closed at $**{IEXData['latestPrice']}**,"
+ f" after hours _(15 minutes delayed)_ the stock price is $**{IEXData['extendedPrice']}**"
)
change = round(IEXData["extendedChangePercent"] * 100, 2)
# Determine wording of change text
if change > 0:
message += f", the stock is currently **up {change}%**"
elif change < 0:
message += f", the stock is currently **down {change}%**"
else:
message += ", the stock hasn't shown any movement today."
else:
message = (
f"The symbol: {symbol} encountered and error. This could be due to "
)
else:
message = f"The symbol: {symbol} was not found."
return message
def dividend_reply(self, symbol: Stock) -> str:
"""Returns the most recent, or next dividend date for a stock symbol.
Parameters
----------
symbols : list
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 div dates.
"""
IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/dividends/next?token={self.IEX_TOKEN}"
response = r.get(IEXurl)
if response.status_code == 200 and response.json():
IEXData = response.json()[0]
keys = (
"amount",
"currency",
"declaredDate",
"exDate",
"frequency",
"paymentDate",
"flag",
)
if set(keys).issubset(IEXData):
if IEXData["currency"] == "USD":
price = f"${IEXData['amount']}"
else:
price = f"{IEXData['amount']} {IEXData['currency']}"
# Pattern IEX uses for dividend date.
pattern = "%Y-%m-%d"
declared = datetime.strptime(IEXData["declaredDate"], pattern).strftime(
"%A, %B %w"
)
ex = datetime.strptime(IEXData["exDate"], pattern).strftime("%A, %B %w")
payment = datetime.strptime(IEXData["paymentDate"], pattern).strftime(
"%A, %B %w"
)
daysDelta = (
datetime.strptime(IEXData["paymentDate"], pattern) - datetime.now()
).days
return (
"The next dividend for"
+ f"${self.symbol_list[self.symbol_list['symbol']==symbol.id.upper()]['description'].item()}"
+ f" is on {payment} which is in {daysDelta} days."
+ f" The dividend is for {price} per share."
+ f"\nThe dividend was declared on {declared} and the ex-dividend date is {ex}"
)
return f"{symbol} either doesn't exist or pays no dividend."
def news_reply(self, symbol: Stock) -> str:
"""Gets recent english news on stock symbols.
Parameters
----------
symbols : list
List of stock symbols.
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.
"""
IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/news/last/5?token={self.IEX_TOKEN}"
response = r.get(IEXurl)
if response.status_code == 200:
data = response.json()
if len(data):
message = f"News for **{symbol.id.upper()}**:\n\n"
for news in data:
if news["lang"] == "en" and not news["hasPaywall"]:
message = (
f"*{news['source']}*: [{news['headline']}]({news['url']})\n"
)
message += message
else:
return f"No news found for: {symbol}\nEither today is boring or the symbol does not exist."
else:
return f"No news found for: {symbol}\nEither today is boring or the symbol does not exist."
return message
def info_reply(self, symbol: Stock) -> str:
"""Gets information on stock symbols.
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 information.
"""
IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/company?token={self.IEX_TOKEN}"
response = r.get(IEXurl)
if response.status_code == 200:
data = response.json()
message = (
f"Company Name: [{data['companyName']}]({data['website']})\nIndustry:"
+ f" {data['industry']}\nSector: {data['sector']}\nCEO: {data['CEO']}\nDescription: {data['description']}\n"
)
else:
message = f"No information found for: {symbol}\nEither today is boring or the symbol does not exist."
return message
def intra_reply(self, symbol: Stock) -> pd.DataFrame:
"""Returns price data for a symbol since the last market open.
Parameters
----------
symbol : str
Stock symbol.
Returns
-------
pd.DataFrame
Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
"""
if symbol.id.upper() not in list(self.symbol_list["symbol"]):
return pd.DataFrame()
IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/intraday-prices?token={self.IEX_TOKEN}"
response = r.get(IEXurl)
if response.status_code == 200:
df = pd.DataFrame(response.json())
df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"])
df["DT"] = pd.to_datetime(df["date"] + "T" + df["minute"])
df = df.set_index("DT")
return df
return pd.DataFrame()
def chart_reply(self, symbol: Stock) -> pd.DataFrame:
"""Returns price data for a symbol of the past month up until the previous trading days close.
Also caches multiple requests made in the same day.
Parameters
----------
symbol : str
Stock symbol.
Returns
-------
pd.DataFrame
Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
"""
schedule.run_pending()
if symbol.id.upper() not in list(self.symbol_list["symbol"]):
return pd.DataFrame()
try: # https://stackoverflow.com/a/3845776/8774114
return self.charts[symbol.id.upper()]
except KeyError:
pass
response = r.get(
f"https://cloud.iexapis.com/stable/stock/{symbol}/chart/1mm?token={self.IEX_TOKEN}&chartInterval=3&includeToday=false"
)
if response.status_code == 200:
df = pd.DataFrame(response.json())
df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"])
df["DT"] = pd.to_datetime(df["date"] + "T" + df["minute"])
df = df.set_index("DT")
self.charts[symbol.id.upper()] = df
return df
return pd.DataFrame()
def stat_reply(self, symbol: Stock) -> str:
"""Gets key statistics 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 statistics.
"""
IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/stats?token={self.IEX_TOKEN}"
response = r.get(IEXurl)
if response.status_code == 200:
data = response.json()
[data.pop(k) for k in list(data) if data[k] == ""]
m = ""
if "companyName" in data:
m += f"Company Name: {data['companyName']}\n"
if "marketcap" in data:
m += f"Market Cap: {data['marketcap']:,}\n"
if "week52high" in data:
m += f"52 Week (high-low): {data['week52high']:,} "
if "week52low" in data:
m += f"- {data['week52low']:,}\n"
if "employees" in data:
m += f"Number of Employees: {data['employees']:,}\n"
if "nextEarningsDate" in data:
m += f"Next Earnings Date: {data['nextEarningsDate']}\n"
if "peRatio" in data:
m += f"Price to Earnings: {data['peRatio']:.3f}\n"
if "beta" in data:
m += f"Beta: {data['beta']:.3f}\n"
return m
else:
return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist."

51
Symbol.py Normal file
View File

@ -0,0 +1,51 @@
import requests as r
class Symbol:
"""
symbol: What the user calls it. ie tsla or btc
id: What the api expects. ie tsla or bitcoin
name: Human readable. ie Tesla or Bitcoin
"""
currency = "usd"
pass
def __init__(self, symbol) -> None:
self.symbol = symbol
self.id = symbol
self.name = symbol
def __repr__(self) -> str:
return f"<{self.__class__.__name__} instance of {self.id} at {id(self)}>"
def __str__(self) -> str:
return self.id
class Stock(Symbol):
def __init__(self, symbol: str) -> None:
self.symbol = symbol
self.id = symbol
self.name = "$" + symbol.upper()
# Used by Coin to change symbols for ids
coins = r.get("https://api.coingecko.com/api/v3/coins/list").json()
class Coin(Symbol):
def __init__(self, symbol: str) -> None:
self.symbol = symbol
self.get_data()
def get_data(self) -> None:
self.id = list(filter(lambda coin: coin["symbol"] == self.symbol, coins))[0][
"id"
]
data = r.get("https://api.coingecko.com/api/v3/coins/" + self.id).json()
self.data = data
self.name = data["name"]
self.description = data["description"]
self.price = data["market_data"]["current_price"][self.currency]

70
T_info.py Normal file
View File

@ -0,0 +1,70 @@
"""Functions and Info specific to the Telegram Bot
"""
import re
import requests as r
class T_info:
license = re.sub(
r"\b\n",
" ",
r.get(
"https://gitlab.com/simple-stock-bots/simple-telegram-stock-bot/-/raw/master/LICENSE"
).text,
)
help_text = """
Thanks for using this bot, consider supporting it by [buying me a beer.](https://www.buymeacoffee.com/Anson)
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)
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.
**Commands**
- /donate [amount in USD] to donate. 🎗
- /dividend $[symbol] will return dividend information for the symbol. 📅
- /intra $[symbol] Plot of the stocks movement since the last market open. 📈
- /chart $[symbol] Plot of the stocks movement for the past 1 month. 📊
- /news $[symbol] News about the symbol. 📰
- /info $[symbol] General information about the symbol.
- /stat $[symbol] Key statistics about the symbol. 🔢
- /help Get some help using the bot. 🆘
**Inline Features**
You can type @SimpleStockBot `[search]` in any chat or direct message to search for the stock bots full list of stock symbols and return the price of the ticker. Then once you select the ticker want the bot will send a message as you in that chat with the latest stock price.
Market data is provided by [IEX Cloud](https://iexcloud.io)
If you believe the bot is not behaving properly run `/status`.
"""
donate_text = """
Simple Stock Bot is run entirely on donations[.](https://www.buymeacoffee.com/Anson)
All donations go directly towards paying for servers, and market data is provided by
[IEX Cloud](https://iexcloud.io/).
The easiest way to donate is to run the `/donate [amount in USD]` command with US dollars you would like to donate.
Example: `/donate 2` would donate 2 USD.
An alternative way to donate is through https://www.buymeacoffee.com/Anson,which accepts Paypal or Credit card.
If you have any questions get in touch: @MisterBiggs or [anson@ansonbiggs.com](http://mailto:anson@ansonbiggs.com/)
_Donations can only be made in a chat directly with @simplestockbot_
"""
commands = """
donate - Donate to the bot 🎗
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. 🔢
dividend - $[symbol] Dividend info 📅
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

165
bot.py
View File

@ -3,7 +3,6 @@ import datetime
import io
import logging
import os
import random
import html
import json
import traceback
@ -26,22 +25,20 @@ from telegram.ext import (
CallbackContext,
)
from functions import Symbol
from symbol_router import Router
from T_info import T_info
TELEGRAM_TOKEN = os.environ["TELEGRAM"]
try:
IEX_TOKEN = os.environ["IEX"]
except KeyError:
IEX_TOKEN = ""
print("Starting without an IEX Token will not allow you to get market data!")
try:
STRIPE_TOKEN = os.environ["STRIPE"]
except KeyError:
STRIPE_TOKEN = ""
print("Starting without a STRIPE Token will not allow you to accept Donations!")
s = Symbol(IEX_TOKEN)
s = Router()
t = T_info()
# Enable logging
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
@ -53,42 +50,28 @@ print("Bot Online")
def start(update: Update, context: CallbackContext):
"""Send a message when the command /start is issued."""
update.message.reply_text(text=s.help_text, parse_mode=telegram.ParseMode.MARKDOWN)
update.message.reply_text(text=t.help_text, parse_mode=telegram.ParseMode.MARKDOWN)
def help(update: Update, context: CallbackContext):
"""Send link to docs when the command /help is issued."""
update.message.reply_text(text=s.help_text, parse_mode=telegram.ParseMode.MARKDOWN)
update.message.reply_text(text=t.help_text, parse_mode=telegram.ParseMode.MARKDOWN)
def license(update: Update, context: CallbackContext):
"""Return bots license agreement"""
update.message.reply_text(text=s.license, parse_mode=telegram.ParseMode.MARKDOWN)
update.message.reply_text(text=t.license, parse_mode=telegram.ParseMode.MARKDOWN)
def status(update: Update, context: CallbackContext):
message = ""
try:
# Bot Status
bot_resp = (
datetime.datetime.now(update.message.date.tzinfo) - update.message.date
)
message += f"It took {bot_resp.total_seconds()} seconds for the bot to get your message.\n"
bot_resp = datetime.datetime.now(update.message.date.tzinfo) - update.message.date
# IEX Status
message += s.iex_status() + "\n"
# Message Status
message += s.message_status()
except Exception as ex:
message += (
f"*\n\nERROR ENCOUNTERED:*\n{ex}\n\n"
+ "*The bot encountered an error while attempting to find errors. Please contact the bot admin.*"
)
update.message.reply_text(text=message, parse_mode=telegram.ParseMode.MARKDOWN)
update.message.reply_text(
text=s.status(
f"It took {bot_resp.total_seconds()} seconds for the bot to get your message."
),
parse_mode=telegram.ParseMode.MARKDOWN,
)
def donate(update: Update, context: CallbackContext):
@ -96,7 +79,7 @@ def donate(update: Update, context: CallbackContext):
if update.message.text.strip() == "/donate":
update.message.reply_text(
text=s.donate_text, parse_mode=telegram.ParseMode.MARKDOWN
text=t.donate_text, parse_mode=telegram.ParseMode.MARKDOWN
)
return
else:
@ -157,11 +140,10 @@ 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)
for reply in s.price_reply(symbols).items():
print(symbols)
for reply in s.price_reply(symbols):
update.message.reply_text(
text=reply[1], parse_mode=telegram.ParseMode.MARKDOWN
text=reply, parse_mode=telegram.ParseMode.MARKDOWN
)
@ -182,9 +164,9 @@ def dividend(update: Update, context: CallbackContext):
if symbols:
context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING)
for symbol in symbols:
for reply in s.dividend_reply(symbols):
update.message.reply_text(
text=s.dividend_reply(symbol), parse_mode=telegram.ParseMode.MARKDOWN
text=reply, parse_mode=telegram.ParseMode.MARKDOWN
)
@ -206,9 +188,9 @@ def news(update: Update, context: CallbackContext):
if symbols:
context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING)
for reply in s.news_reply(symbols).items():
for reply in s.news_reply(symbols):
update.message.reply_text(
text=reply[1], parse_mode=telegram.ParseMode.MARKDOWN
text=reply, parse_mode=telegram.ParseMode.MARKDOWN
)
@ -230,9 +212,9 @@ def info(update: Update, context: CallbackContext):
if symbols:
context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING)
for reply in s.info_reply(symbols).items():
for reply in s.info_reply(symbols):
update.message.reply_text(
text=reply[1], parse_mode=telegram.ParseMode.MARKDOWN
text=reply, parse_mode=telegram.ParseMode.MARKDOWN
)
@ -267,7 +249,14 @@ def intra(update: Update, context: CallbackContext):
)
return
symbol = s.find_symbols(message)[0]
symbols = s.find_symbols(message)
symbol = symbols[0]
if len(symbols):
symbol = symbols[0]
else:
update.message.reply_text("No symbols or coins found.")
return
df = s.intra_reply(symbol)
if df.empty:
@ -285,8 +274,8 @@ def intra(update: Update, context: CallbackContext):
mpf.plot(
df,
type="renko",
title=f"\n${symbol.upper()}",
volume=True,
title=f"\n{symbol.name}",
volume="volume" in df.keys(),
style="yahoo",
mav=20,
savefig=dict(fname=buf, dpi=400, bbox_inches="tight"),
@ -295,9 +284,9 @@ def intra(update: Update, context: CallbackContext):
update.message.reply_photo(
photo=buf,
caption=f"\nIntraday chart for ${symbol.upper()} from {df.first_valid_index().strftime('%I:%M')} to"
caption=f"\nIntraday chart for {symbol.name} from {df.first_valid_index().strftime('%I:%M')} to"
+ f" {df.last_valid_index().strftime('%I:%M')} ET on"
+ f" {datetime.date.today().strftime('%d, %b %Y')}\n\n{s.price_reply([symbol])[symbol]}",
+ f" {datetime.date.today().strftime('%d, %b %Y')}\n\n{s.price_reply([symbol])[0]}",
parse_mode=telegram.ParseMode.MARKDOWN,
)
@ -314,7 +303,13 @@ def chart(update: Update, context: CallbackContext):
)
return
symbol = s.find_symbols(message)[0]
symbols = s.find_symbols(message)
if len(symbols):
symbol = symbols[0]
else:
update.message.reply_text("No symbols or coins found.")
return
df = s.chart_reply(symbol)
if df.empty:
@ -323,17 +318,16 @@ def chart(update: Update, context: CallbackContext):
parse_mode=telegram.ParseMode.MARKDOWN,
)
return
context.bot.send_chat_action(
chat_id=chat_id, action=telegram.ChatAction.UPLOAD_PHOTO
)
print(symbol)
buf = io.BytesIO()
mpf.plot(
df,
type="candle",
title=f"\n${symbol.upper()}",
volume=True,
title=f"\n{symbol.name}",
volume="volume" in df.keys(),
style="yahoo",
savefig=dict(fname=buf, dpi=400, bbox_inches="tight"),
)
@ -341,8 +335,8 @@ def chart(update: Update, context: CallbackContext):
update.message.reply_photo(
photo=buf,
caption=f"\n1 Month chart for ${symbol.upper()} from {df.first_valid_index().strftime('%d, %b %Y')}"
+ f" to {df.last_valid_index().strftime('%d, %b %Y')}\n\n{s.price_reply([symbol])[symbol]}",
caption=f"\n1 Month chart for {symbol.name} from {df.first_valid_index().strftime('%d, %b %Y')}"
+ f" to {df.last_valid_index().strftime('%d, %b %Y')}\n\n{s.price_reply([symbol])[0]}",
parse_mode=telegram.ParseMode.MARKDOWN,
)
@ -365,38 +359,12 @@ def stat(update: Update, context: CallbackContext):
if symbols:
context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING)
for reply in s.stat_reply(symbols).items():
for reply in s.stat_reply(symbols):
update.message.reply_text(
text=reply[1], parse_mode=telegram.ParseMode.MARKDOWN
text=reply, parse_mode=telegram.ParseMode.MARKDOWN
)
def crypto(update: Update, context: CallbackContext):
"""
https://iexcloud.io/docs/api/#cryptocurrency-quote
"""
context.bot.send_chat_action(
chat_id=update.message.chat_id, action=telegram.ChatAction.TYPING
)
message = update.message.text
if message.strip() == "/crypto":
update.message.reply_text(
"This command returns the current price in USD for a cryptocurrency.\nExample: /crypto eth"
)
return
reply = s.crypto_reply(message)
if reply:
update.message.reply_text(text=reply, parse_mode=telegram.ParseMode.MARKDOWN)
else:
update.message.reply_text(
text=f"Pair: {message} returned an error.",
parse_mode=telegram.ParseMode.MARKDOWN,
)
def inline_query(update: Update, context: CallbackContext):
"""
Handles inline query.
@ -408,7 +376,7 @@ def inline_query(update: Update, context: CallbackContext):
results = []
for match in matches:
try:
price = s.price_reply([match[0]])[match[0]]
price = s.price_reply([match[0]])[0]
results.append(
InlineQueryResultArticle(
match[0],
@ -429,13 +397,8 @@ def inline_query(update: Update, context: CallbackContext):
def rand_pick(update: Update, context: CallbackContext):
choice = random.choice(list(s.symbol_list["description"]))
hold = (
datetime.date.today() + datetime.timedelta(random.randint(1, 365))
).strftime("%b %d, %Y")
update.message.reply_text(
text=f"{choice}\nBuy and hold until: {hold}",
text=s.random_pick(),
parse_mode=telegram.ParseMode.MARKDOWN,
)
@ -448,7 +411,18 @@ def error(update: Update, context: CallbackContext):
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>"
)
<<<<<<< HEAD
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))}"
@ -462,6 +436,11 @@ def error(update: Update, context: CallbackContext):
print(message)
# update.message.reply_text(text=message, parse_mode=telegram.ParseMode.HTML)
# update.message.reply_text(text="Please inform the bot admin of this issue.")
=======
# Finally, send the message
update.message.reply_text(text=message, parse_mode=telegram.ParseMode.HTML)
update.message.reply_text(text="Please inform the bot admin of this issue.")
>>>>>>> crypto
def main():
@ -485,8 +464,7 @@ def main():
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))
dp.add_handler(CommandHandler("crypto", crypto))
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))
@ -511,9 +489,6 @@ def main():
# Start the Bot
updater.start_polling()
# Run the bot until you press Ctrl-C or the process receives SIGINT,
# SIGTERM or SIGABRT. This should be used most of the time, since
# start_polling() is non-blocking and will stop the bot gracefully.
updater.idle()

261
cg_Crypto.py Normal file
View File

@ -0,0 +1,261 @@
"""Class with functions for running the bot with IEX Cloud.
"""
from datetime import datetime
from typing import Optional, List, Tuple
import pandas as pd
import requests as r
import schedule
from fuzzywuzzy import fuzz
from markdownify import markdownify
from Symbol import Coin
class cg_Crypto:
"""
Functions for finding crypto info
"""
vs_currency = "usd" # simple/supported_vs_currencies for list of options
searched_symbols = {}
def __init__(self) -> None:
"""Creates a Symbol Object
Parameters
----------
IEX_TOKEN : str
IEX Token
"""
self.get_symbol_list()
schedule.every().day.do(self.get_symbol_list)
def symbol_id(self, symbol) -> str:
try:
return self.symbol_list[self.symbol_list["symbol"] == symbol]["id"].values[
0
]
except KeyError:
return ""
def get_symbol_list(
self, return_df=False
) -> Optional[Tuple[pd.DataFrame, datetime]]:
raw_symbols = r.get("https://api.coingecko.com/api/v3/coins/list").json()
symbols = pd.DataFrame(data=raw_symbols)
symbols["description"] = symbols["symbol"] + ": " + symbols["name"]
self.symbol_list = symbols
if return_df:
return symbols, datetime.now()
def status(self) -> str:
"""Checks CoinGecko /ping endpoint for API issues.
Returns
-------
str
Human readable text on status of CoinGecko API
"""
status = r.get("https://api.coingecko.com/api/v3/ping")
if status.status_code == 200:
return f"CoinGecko API responded that it was OK in {status.elapsed.total_seconds()} Seconds."
else:
return f"CoinGecko API returned an error in {status.elapsed.total_seconds()} Seconds."
def search_symbols(self, search: str) -> List[Tuple[str, str]]:
"""Performs a fuzzy search to find coin symbols closest to a search term.
Parameters
----------
search : str
String used to search, could be a company name or something close to the companies coin ticker.
Returns
-------
List[tuple[str, str]]
A list tuples of every coin sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name).
"""
schedule.run_pending()
search = search.lower()
try: # https://stackoverflow.com/a/3845776/8774114
return self.searched_symbols[search]
except KeyError:
pass
symbols = self.symbol_list
symbols["Match"] = symbols.apply(
lambda x: fuzz.ratio(search, f"{x['symbol']}".lower()),
axis=1,
)
symbols.sort_values(by="Match", ascending=False, inplace=True)
if symbols["Match"].head().sum() < 300:
symbols["Match"] = symbols.apply(
lambda x: fuzz.partial_ratio(search, x["name"].lower()),
axis=1,
)
symbols.sort_values(by="Match", ascending=False, inplace=True)
symbols = symbols.head(10)
symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"])))
self.searched_symbols[search] = symbol_list
return symbol_list
def price_reply(self, symbol: Coin) -> str:
"""Returns current market price or after hours if its available for a given coin symbol.
Parameters
----------
symbols : list
List of coin symbols.
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.
"""
response = r.get(
f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false"
)
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"]
except KeyError:
return f"{symbol} returned an error."
message = f"The current price of {name} is $**{price:,}**"
# Determine wording of change text
if change > 0:
message += f", the coin is currently **up {change:.3f}%** for today"
elif change < 0:
message += f", the coin is currently **down {change:.3f}%** for today"
else:
message += ", the coin hasn't shown any movement today."
else:
message = f"The Coin: {symbol.name} was not found."
return message
def intra_reply(self, symbol: Coin) -> pd.DataFrame:
"""Returns price data for a symbol since the last market open.
Parameters
----------
symbol : str
Stock symbol.
Returns
-------
pd.DataFrame
Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
"""
response = r.get(
f"https://api.coingecko.com/api/v3/coins/{symbol.id}/ohlc?vs_currency=usd&days=1"
)
if response.status_code == 200:
df = pd.DataFrame(
response.json(), columns=["Date", "Open", "High", "Low", "Close"]
).dropna()
df["Date"] = pd.to_datetime(df["Date"], unit="ms")
df = df.set_index("Date")
return df
return pd.DataFrame()
def chart_reply(self, symbol: Coin) -> pd.DataFrame:
"""Returns price data for a symbol of the past month up until the previous trading days close.
Also caches multiple requests made in the same day.
Parameters
----------
symbol : str
Stock symbol.
Returns
-------
pd.DataFrame
Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
"""
response = r.get(
f"https://api.coingecko.com/api/v3/coins/{symbol.id}/ohlc?vs_currency=usd&days=30"
)
if response.status_code == 200:
df = pd.DataFrame(
response.json(), columns=["Date", "Open", "High", "Low", "Close"]
).dropna()
df["Date"] = pd.to_datetime(df["Date"], unit="ms")
df = df.set_index("Date")
return df
return pd.DataFrame()
def stat_reply(self, symbol: Coin) -> str:
"""Gets key statistics for each symbol in the list
Parameters
----------
symbols : List[str]
List of coin 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.
"""
response = r.get(
f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false"
)
if response.status_code == 200:
data = response.json()
return f"""
[{data['name']}]({data['links']['homepage'][0]}) Statistics:
Maket Cap Ranking: {data.get('market_cap_rank',"Not Available")}
CoinGecko Scores:
Overall: {data.get('coingecko_score','Not Available')}
Development: {data.get('developer_score','Not Available')}
Community: {data.get('community_score','Not Available')}
Public Interest: {data.get('public_interest_score','Not Available')}
"""
else:
return f"{symbol.symbol} returned an error."
def info_reply(self, symbol: Coin) -> str:
"""Gets information on stock symbols.
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 information.
"""
response = r.get(
f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false"
)
if response.status_code == 200:
data = response.json()
try:
return markdownify(data["description"]["en"])
except KeyError:
return f"{symbol} does not have a description available."
return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist."

View File

@ -1,8 +0,0 @@
donate - Donate to the bot 🎗️
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. 🔢
dividend - $[symbol] Dividend info 📅
intra - $[symbol] Plot since the last market open. 📈
chart - $[chart] Plot of the past month. 📊

View File

@ -1,553 +0,0 @@
import re
from datetime import datetime
from typing import Optional, List, Tuple, Dict
import pandas as pd
import requests as r
import schedule
from fuzzywuzzy import fuzz
class Symbol:
"""
Functions for finding stock market information about symbols.
"""
SYMBOL_REGEX = "[$]([a-zA-Z]{1,4})"
searched_symbols = {}
charts = {}
license = re.sub(
r"\b\n",
" ",
r.get(
"https://gitlab.com/simple-stock-bots/simple-telegram-stock-bot/-/raw/master/LICENSE"
).text,
)
help_text = """
Thanks for using this bot, consider supporting it by [buying me a beer.](https://www.buymeacoffee.com/Anson)
Keep up with the latest news for the bot in itsTelegram Channel: https://t.me/simplestockbotnews
Full documentation on using and running your own stock bot can be found [here.](https://simple-stock-bots.gitlab.io/site)
**Commands**
- /donate [amount in USD] to donate. 🎗
- /dividend $[symbol] will return dividend information for the symbol. 📅
- /intra $[symbol] Plot of the stocks movement since the last market open. 📈
- /chart $[symbol] Plot of the stocks movement for the past 1 month. 📊
- /news $[symbol] News about the symbol. 📰
- /info $[symbol] General information about the symbol.
- /stat $[symbol] Key statistics about the symbol. 🔢
- /help Get some help using the bot. 🆘
**Inline Features**
You can type @SimpleStockBot `[search]` in any chat or direct message to search for the stock bots
full list of stock symbols and return the price of the ticker. Then once you select the ticker
want the bot will send a message as you in that chat with the latest stock price.
The bot also looks at every message in any chat it is in for stock symbols.Symbols start with a
`$` followed by the stock symbol. For example:$tsla would return price information for Tesla Motors.
Market data is provided by [IEX Cloud](https://iexcloud.io)
If you believe the bot is not behaving properly run `/status`.
"""
donate_text = """
Simple Stock Bot is run entirely on donations[.](https://www.buymeacoffee.com/Anson)
All donations go directly towards paying for servers, and market data is provided by
[IEX Cloud](https://iexcloud.io/).
The easiest way to donate is to run the `/donate [amount in USD]` command with USdollars you would like to donate.
Example: `/donate 2` would donate 2 USD.
An alternative way to donate is through https://www.buymeacoffee.com/Anson,which accepts Paypal or Credit card.
If you have any questions get in touch: @MisterBiggs or[anson@ansonbiggs.com](http://mailto:anson@ansonbiggs.com/)
_Donations can only be made in a chat directly with @simplestockbot_
"""
def __init__(self, IEX_TOKEN: str) -> None:
"""Creates a Symbol Object
Parameters
----------
IEX_TOKEN : str
IEX Token
"""
self.IEX_TOKEN = IEX_TOKEN
if IEX_TOKEN != "":
self.get_symbol_list()
schedule.every().day.do(self.get_symbol_list)
schedule.every().day.do(self.clear_charts)
def clear_charts(self) -> None:
"""Clears cache of chart data."""
self.charts = {}
def get_symbol_list(self, return_df=False) -> Optional[pd.DataFrame]:
raw_symbols = r.get(
f"https://cloud.iexapis.com/stable/ref-data/symbols?token={self.IEX_TOKEN}"
).json()
symbols = pd.DataFrame(data=raw_symbols)
symbols["description"] = symbols["symbol"] + ": " + symbols["name"]
self.symbol_list = symbols
if return_df:
return symbols, datetime.now()
def iex_status(self) -> str:
"""Checks IEX Status dashboard for any current API issues.
Returns
-------
str
Human readable text on status of IEX API
"""
status = r.get("https://pjmps0c34hp7.statuspage.io/api/v2/status.json").json()[
"status"
]
if status["indicator"] == "none":
return "IEX Cloud is currently not reporting any issues with its API."
else:
return (
f"{status['indicator']}: {status['description']}."
+ " Please check the status page for more information. https://status.iexapis.com"
)
def message_status(self) -> str:
"""Checks to see if the bot has available IEX Credits
Returns
-------
str
Human readable text on status of IEX Credits.
"""
usage = r.get(
f"https://cloud.iexapis.com/stable/account/metadata?token={self.IEX_TOKEN}"
).json()
try:
if (
usage["messagesUsed"] >= usage["messageLimit"] - 10000
and not usage["payAsYouGoEnabled"]
):
return "Bot may be out of IEX Credits."
else:
return "Bot has available IEX Credits."
except KeyError:
return "**IEX API could not be reached.**"
def search_symbols(self, search: str) -> List[Tuple[str, str]]:
"""Performs a fuzzy search to find stock symbols closest to a search term.
Parameters
----------
search : str
String used to search, could be a company name or something close to the companies stock ticker.
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).
"""
schedule.run_pending()
search = search.lower()
try: # https://stackoverflow.com/a/3845776/8774114
return self.searched_symbols[search]
except KeyError:
pass
symbols = self.symbol_list
symbols["Match"] = symbols.apply(
lambda x: fuzz.ratio(search, f"{x['symbol']}".lower()),
axis=1,
)
symbols.sort_values(by="Match", ascending=False, inplace=True)
if symbols["Match"].head().sum() < 300:
symbols["Match"] = symbols.apply(
lambda x: fuzz.partial_ratio(search, x["name"].lower()),
axis=1,
)
symbols.sort_values(by="Match", ascending=False, inplace=True)
symbols = symbols.head(10)
symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"])))
self.searched_symbols[search] = symbol_list
return symbol_list
def find_symbols(self, text: str) -> List[str]:
"""Finds stock tickers starting with a dollar sign in a blob of text and returns them in a list.
Only returns each match once. Example: Whats the price of $tsla?
Parameters
----------
text : str
Blob of text.
Returns
-------
List[str]
List of stock symbols as strings without dollar sign.
"""
return list(set(re.findall(self.SYMBOL_REGEX, text)))
def price_reply(self, symbols: list) -> Dict[str, str]:
"""Returns current market price or after hours if its available for a given stock symbol.
Parameters
----------
symbols : list
List of stock symbols.
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.
"""
dataMessages = {}
for symbol in symbols:
IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/quote?token={self.IEX_TOKEN}"
response = r.get(IEXurl)
if response.status_code == 200:
IEXData = response.json()
keys = (
"isUSMarketOpen",
"extendedChangePercent",
"extendedPrice",
"companyName",
"latestPrice",
"changePercent",
)
if set(keys).issubset(IEXData):
try: # Some symbols dont return if the market is open
IEXData["isUSMarketOpen"]
except KeyError:
IEXData["isUSMarketOpen"] = True
if (
IEXData["isUSMarketOpen"]
or (IEXData["extendedChangePercent"] is None)
or (IEXData["extendedPrice"] is None)
): # Check if market is open.
message = f"The current stock price of {IEXData['companyName']} is $**{IEXData['latestPrice']}**"
try:
change = round(IEXData["changePercent"] * 100, 2)
except (KeyError, TypeError):
change = 0
else:
message = (
f"{IEXData['companyName']} closed at $**{IEXData['latestPrice']}**,"
+ f" after hours _(15 minutes delayed)_ the stock price is $**{IEXData['extendedPrice']}**"
)
change = round(IEXData["extendedChangePercent"] * 100, 2)
# Determine wording of change text
if change > 0:
message += f", the stock is currently **up {change}%**"
elif change < 0:
message += f", the stock is currently **down {change}%**"
else:
message += ", the stock hasn't shown any movement today."
else:
message = f"The symbol: {symbol} encountered and error. This could be due to the symbol not being fully supported by IEX Cloud."
else:
message = f"The symbol: {symbol} was not found."
if symbol.upper() == "GME":
message += "\n\n🙌💎Power to the Players💎🙌"
if IEXData["latestPrice"] is None:
message = f"{symbol} has not reported price info to IEX Cloud."
dataMessages[symbol] = message
return dataMessages
def dividend_reply(self, symbol: str) -> Dict[str, str]:
"""Returns the most recent, or next dividend date for a stock symbol.
Parameters
----------
symbols : list
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 div dates.
"""
IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/dividends/next?token={self.IEX_TOKEN}"
response = r.get(IEXurl)
if response.status_code == 200 and response.json():
IEXData = response.json()[0]
keys = (
"amount",
"currency",
"declaredDate",
"exDate",
"frequency",
"paymentDate",
"flag",
)
if set(keys).issubset(IEXData):
if IEXData["currency"] == "USD":
price = f"${IEXData['amount']}"
else:
price = f"{IEXData['amount']} {IEXData['currency']}"
# Pattern IEX uses for dividend date.
pattern = "%Y-%m-%d"
declared = datetime.strptime(IEXData["declaredDate"], pattern).strftime(
"%A, %B %w"
)
ex = datetime.strptime(IEXData["exDate"], pattern).strftime("%A, %B %w")
payment = datetime.strptime(IEXData["paymentDate"], pattern).strftime(
"%A, %B %w"
)
daysDelta = (
datetime.strptime(IEXData["paymentDate"], pattern) - datetime.now()
).days
return (
f"The next dividend for ${self.symbol_list[self.symbol_list['symbol']==symbol.upper()]['description'].item()}"
+ f" is on {payment} which is in {daysDelta} days."
+ f" The dividend is for {price} per share."
+ f"\nThe dividend was declared on {declared} and the ex-dividend date is {ex}"
)
return f"{symbol} either doesn't exist or pays no dividend."
def news_reply(self, symbols: list) -> Dict[str, str]:
"""Gets recent english news on stock symbols.
Parameters
----------
symbols : list
List of stock symbols.
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.
"""
newsMessages = {}
for symbol in symbols:
IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/news/last/5?token={self.IEX_TOKEN}"
response = r.get(IEXurl)
if response.status_code == 200:
data = response.json()
if len(data):
newsMessages[symbol] = f"News for **{symbol.upper()}**:\n\n"
for news in data:
if news["lang"] == "en" and not news["hasPaywall"]:
message = f"*{news['source']}*: [{news['headline']}]({news['url']})\n"
newsMessages[symbol] = newsMessages[symbol] + message
else:
newsMessages[
symbol
] = f"No news found for: {symbol}\nEither today is boring or the symbol does not exist."
else:
newsMessages[
symbol
] = f"No news found for: {symbol}\nEither today is boring or the symbol does not exist."
return newsMessages
def info_reply(self, symbols: List[str]) -> Dict[str, str]:
"""Gets information on stock symbols.
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 information.
"""
infoMessages = {}
for symbol in symbols:
IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/company?token={self.IEX_TOKEN}"
response = r.get(IEXurl)
if response.status_code == 200:
data = response.json()
infoMessages[symbol] = (
f"Company Name: [{data['companyName']}]({data['website']})\nIndustry:"
+ f" {data['industry']}\nSector: {data['sector']}\nCEO: {data['CEO']}\nDescription: {data['description']}\n"
)
else:
infoMessages[
symbol
] = f"No information found for: {symbol}\nEither today is boring or the symbol does not exist."
return infoMessages
def intra_reply(self, symbol: str) -> pd.DataFrame:
"""Returns price data for a symbol since the last market open.
Parameters
----------
symbol : str
Stock symbol.
Returns
-------
pd.DataFrame
Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
"""
if symbol.upper() not in list(self.symbol_list["symbol"]):
return pd.DataFrame()
IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/intraday-prices?token={self.IEX_TOKEN}"
response = r.get(IEXurl)
if response.status_code == 200:
df = pd.DataFrame(response.json())
df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"])
df["DT"] = pd.to_datetime(df["date"] + "T" + df["minute"])
df = df.set_index("DT")
return df
return pd.DataFrame()
def chart_reply(self, symbol: str) -> pd.DataFrame:
"""Returns price data for a symbol of the past month up until the previous trading days close.
Also caches multiple requests made in the same day.
Parameters
----------
symbol : str
Stock symbol.
Returns
-------
pd.DataFrame
Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
"""
schedule.run_pending()
if symbol.upper() not in list(self.symbol_list["symbol"]):
return pd.DataFrame()
try: # https://stackoverflow.com/a/3845776/8774114
return self.charts[symbol.upper()]
except KeyError:
pass
response = r.get(
f"https://cloud.iexapis.com/stable/stock/{symbol}/chart/1mm?token={self.IEX_TOKEN}&chartInterval=3&includeToday=false"
)
if response.status_code == 200:
df = pd.DataFrame(response.json())
df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"])
df["DT"] = pd.to_datetime(df["date"] + "T" + df["minute"])
df = df.set_index("DT")
self.charts[symbol.upper()] = df
return df
return pd.DataFrame()
def stat_reply(self, symbols: List[str]) -> Dict[str, str]:
"""Gets key statistics 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 statistics.
"""
infoMessages = {}
for symbol in symbols:
IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/stats?token={self.IEX_TOKEN}"
response = r.get(IEXurl)
if response.status_code == 200:
data = response.json()
[data.pop(k) for k in list(data) if data[k] == ""]
m = ""
if "companyName" in data:
m += f"Company Name: {data['companyName']}\n"
if "marketcap" in data:
m += f"Market Cap: {data['marketcap']:,}\n"
if "week52high" in data:
m += f"52 Week (high-low): {data['week52high']:,} "
if "week52low" in data:
m += f"- {data['week52low']:,}\n"
if "employees" in data:
m += f"Number of Employees: {data['employees']:,}\n"
if "nextEarningsDate" in data:
m += f"Next Earnings Date: {data['nextEarningsDate']}\n"
if "peRatio" in data:
m += f"Price to Earnings: {data['peRatio']:.3f}\n"
if "beta" in data:
m += f"Beta: {data['beta']:.3f}\n"
infoMessages[symbol] = m
else:
infoMessages[
symbol
] = f"No information found for: {symbol}\nEither today is boring or the symbol does not exist."
return infoMessages
def crypto_reply(self, pair: str) -> str:
"""Returns the current price of a cryptocurrency
Parameters
----------
pair : str
symbol for the cryptocurrency, sometimes with a price pair like ETHUSD
Returns
-------
str
Returns a human readable markdown description of the price, or an empty string if no price was found.
"""
pair = pair.split(" ")[-1].replace("/", "").upper()
pair += "USD" if len(pair) == 3 else pair
IEXurl = f"https://cloud.iexapis.com/stable/crypto/{pair}/quote?token={self.IEX_TOKEN}"
response = r.get(IEXurl)
if response.status_code == 200:
data = response.json()
quote = f"Symbol: {data['symbol']}\n"
quote += f"Price: ${data['latestPrice']}\n"
new, old = data["latestPrice"], data["previousClose"]
if old is not None:
change = (float(new) - float(old)) / float(old)
quote += f"Change: {change}\n"
return quote
else:
return ""

View File

@ -4,4 +4,5 @@ pandas==1.2.1
fuzzywuzzy==0.18.0
python-Levenshtein==0.12.1
schedule==1.0.0
mplfinance==0.12.7a5
mplfinance==0.12.7a5
markdownify==0.6.5

273
symbol_router.py Normal file
View File

@ -0,0 +1,273 @@
"""Function that routes symbols to the correct API provider.
"""
import re
import pandas as pd
import random
import datetime
from typing import List, Tuple
from IEX_Symbol import IEX_Symbol
from cg_Crypto import cg_Crypto
from Symbol import Symbol, Stock, Coin
class Router:
STOCK_REGEX = "(?:^|[^\\$])\\$([a-zA-Z]{1,4})"
CRYPTO_REGEX = "[$]{2}([a-zA-Z]{1,9})"
def __init__(self):
self.stock = IEX_Symbol()
self.crypto = cg_Crypto()
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
----------
text : str
Blob of text.
Returns
-------
List[str]
List of stock symbols as strings without dollar sign.
"""
symbols = []
stocks = set(re.findall(self.STOCK_REGEX, text))
for stock in stocks:
if stock.upper() in self.stock.symbol_list["symbol"].values:
symbols.append(Stock(stock))
else:
print(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)
return symbols
def status(self, bot_resp) -> str:
"""Checks for any issues with APIs.
Returns
-------
str
Human readable text on status of the bot and relevant APIs
"""
return f"""
Bot Status:
{bot_resp}
Stock Market Data:
{self.stock.status()}
Cryptocurrency Data:
{self.crypto.status()}
"""
def search_symbols(self, search: str) -> List[Tuple[str, str]]:
"""Performs a fuzzy search to find stock symbols closest to a search term.
Parameters
----------
search : str
String used to search, could be a company name or something close to the companies stock ticker.
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).
"""
# TODO add support for crypto
return self.stock.search_symbols(search)
def price_reply(self, symbols: list) -> List[str]:
"""Returns current market price or after hours if its available for a given stock symbol.
Parameters
----------
symbols : list
List of stock symbols.
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.
"""
replies = []
for symbol in symbols:
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")
return replies
def dividend_reply(self, symbols: list) -> List[str]:
"""Returns the most recent, or next dividend date for a stock symbol.
Parameters
----------
symbols : list
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 div dates.
"""
replies = []
for symbol in symbols:
if isinstance(symbol, Stock):
replies.append(self.stock.dividend_reply(symbol))
elif isinstance(symbol, Coin):
replies.append("Cryptocurrencies do no have Dividends.")
else:
print(f"{symbol} is not a Stock or Coin")
return replies
def news_reply(self, symbols: list) -> List[str]:
"""Gets recent english news on stock symbols.
Parameters
----------
symbols : list
List of stock symbols.
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.
"""
replies = []
for symbol in symbols:
if isinstance(symbol, Stock):
replies.append(self.stock.news_reply(symbol))
elif isinstance(symbol, Coin):
# replies.append(self.crypto.news_reply(symbol))
replies.append("News is not yet supported for cryptocurrencies.")
else:
print(f"{symbol} is not a Stock or Coin")
return replies
def info_reply(self, symbols: list) -> List[str]:
"""Gets information on stock symbols.
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 information.
"""
replies = []
for symbol in symbols:
if isinstance(symbol, Stock):
replies.append(self.stock.info_reply(symbol))
elif isinstance(symbol, Coin):
replies.append(self.crypto.info_reply(symbol))
else:
print(f"{symbol} is not a Stock or Coin")
return replies
def intra_reply(self, symbol: Symbol) -> pd.DataFrame:
"""Returns price data for a symbol since the last market open.
Parameters
----------
symbol : str
Stock symbol.
Returns
-------
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.intra_reply(symbol)
elif isinstance(symbol, Coin):
return self.crypto.intra_reply(symbol)
else:
print(f"{symbol} is not a Stock or Coin")
return pd.DataFrame()
def chart_reply(self, symbol: Symbol) -> pd.DataFrame:
"""Returns price data for a symbol of the past month up until the previous trading days close.
Also caches multiple requests made in the same day.
Parameters
----------
symbol : str
Stock symbol.
Returns
-------
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)
elif isinstance(symbol, Coin):
return self.crypto.chart_reply(symbol)
else:
print(f"{symbol} is not a Stock or Coin")
return pd.DataFrame()
def stat_reply(self, symbols: List[Symbol]) -> List[str]:
"""Gets key statistics 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 statistics.
"""
replies = []
for symbol in symbols:
if isinstance(symbol, Stock):
replies.append(self.stock.stat_reply(symbol))
elif isinstance(symbol, Coin):
replies.append(self.crypto.stat_reply(symbol))
else:
print(f"{symbol} is not a Stock or Coin")
return replies
def random_pick(self) -> str:
choice = random.choice(
list(self.stock.symbol_list["description"])
+ list(self.crypto.symbol_list["description"])
)
hold = (
datetime.date.today() + datetime.timedelta(random.randint(1, 365))
).strftime("%b %d, %Y")
return f"{choice}\nBuy and hold until: {hold}"