1
0
mirror of https://gitlab.com/simple-stock-bots/simple-telegram-stock-bot.git synced 2025-06-16 15:06:53 +00:00
This commit is contained in:
MisterBiggs 2021-06-23 21:28:13 -07:00
parent baf45e09c8
commit acc0ebdd28
3 changed files with 79 additions and 47 deletions

View File

@ -9,7 +9,7 @@ import requests as r
import schedule import schedule
from fuzzywuzzy import fuzz from fuzzywuzzy import fuzz
import os import os
from logging import warning
from Symbol import Stock from Symbol import Stock
@ -36,7 +36,7 @@ class IEX_Symbol:
self.IEX_TOKEN = os.environ["IEX"] self.IEX_TOKEN = os.environ["IEX"]
except KeyError: except KeyError:
self.IEX_TOKEN = "" self.IEX_TOKEN = ""
print( warning(
"Starting without an IEX Token will not allow you to get market data!" "Starting without an IEX Token will not allow you to get market data!"
) )

72
bot.py
View File

@ -7,6 +7,8 @@ import html
import json import json
import traceback import traceback
from logging import debug, info, warning, error, critical
import mplfinance as mpf import mplfinance as mpf
from uuid import uuid4 from uuid import uuid4
@ -36,7 +38,7 @@ try:
STRIPE_TOKEN = os.environ["STRIPE"] STRIPE_TOKEN = os.environ["STRIPE"]
except KeyError: except KeyError:
STRIPE_TOKEN = "" 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() s = Router()
t = T_info() t = T_info()
@ -47,11 +49,12 @@ logging.basicConfig(
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
print("Bot Online") info("Bot script started.")
def start(update: Update, context: CallbackContext): def start(update: Update, context: CallbackContext):
"""Send a message when the command /start is issued.""" """Send a message when the command /start is issued."""
info(f"Start command ran by {update.message.chat.username}")
update.message.reply_text( update.message.reply_text(
text=t.help_text, text=t.help_text,
parse_mode=telegram.ParseMode.MARKDOWN, parse_mode=telegram.ParseMode.MARKDOWN,
@ -61,6 +64,7 @@ def start(update: Update, context: CallbackContext):
def help(update: Update, context: CallbackContext): def help(update: Update, context: CallbackContext):
"""Send link to docs when the command /help is issued.""" """Send link to docs when the command /help is issued."""
info(f"Help command ran by {update.message.chat.username}")
update.message.reply_text( update.message.reply_text(
text=t.help_text, text=t.help_text,
parse_mode=telegram.ParseMode.MARKDOWN, parse_mode=telegram.ParseMode.MARKDOWN,
@ -70,6 +74,7 @@ def help(update: Update, context: CallbackContext):
def license(update: Update, context: CallbackContext): def license(update: Update, context: CallbackContext):
"""Return bots license agreement""" """Return bots license agreement"""
info(f"License command ran by {update.message.chat.username}")
update.message.reply_text( update.message.reply_text(
text=t.license, text=t.license,
parse_mode=telegram.ParseMode.MARKDOWN, parse_mode=telegram.ParseMode.MARKDOWN,
@ -78,6 +83,7 @@ def license(update: Update, context: CallbackContext):
def status(update: Update, context: CallbackContext): def status(update: Update, context: CallbackContext):
warning(f"Status command ran by {update.message.chat.username}")
bot_resp = datetime.datetime.now(update.message.date.tzinfo) - update.message.date bot_resp = datetime.datetime.now(update.message.date.tzinfo) - update.message.date
update.message.reply_text( update.message.reply_text(
@ -90,6 +96,7 @@ def status(update: Update, context: CallbackContext):
def donate(update: Update, context: CallbackContext): def donate(update: Update, context: CallbackContext):
info(f"Donate command ran by {update.message.chat.username}")
chat_id = update.message.chat_id chat_id = update.message.chat_id
if update.message.text.strip() == "/donate": if update.message.text.strip() == "/donate":
@ -107,7 +114,7 @@ def donate(update: Update, context: CallbackContext):
except ValueError: except ValueError:
update.message.reply_text(f"{amount} is not a valid donation amount or number.") update.message.reply_text(f"{amount} is not a valid donation amount or number.")
return return
print(price) info(f"Donation amount: {price}")
context.bot.send_invoice( context.bot.send_invoice(
chat_id=chat_id, chat_id=chat_id,
@ -126,6 +133,7 @@ def donate(update: Update, context: CallbackContext):
def precheckout_callback(update: Update, context: CallbackContext): def precheckout_callback(update: Update, context: CallbackContext):
info(f"precheckout_callback queried")
query = update.pre_checkout_query query = update.pre_checkout_query
query.answer(ok=True) query.answer(ok=True)
@ -138,6 +146,7 @@ def precheckout_callback(update: Update, context: CallbackContext):
def successful_payment_callback(update: Update, context: CallbackContext): def successful_payment_callback(update: Update, context: CallbackContext):
info(f"Successful payment!")
update.message.reply_text( update.message.reply_text(
"Thank you for your donation! It goes a long way to keeping the bot free!" "Thank you for your donation! It goes a long way to keeping the bot free!"
) )
@ -148,6 +157,7 @@ def symbol_detect(update: Update, context: CallbackContext):
Runs on any message that doesn't have a command and searches for symbols, Runs on any message that doesn't have a command and searches for symbols,
then returns the prices of any symbols found. then returns the prices of any symbols found.
""" """
message = update.message.text message = update.message.text
chat_id = update.message.chat_id chat_id = update.message.chat_id
symbols = s.find_symbols(message) symbols = s.find_symbols(message)
@ -155,7 +165,9 @@ def symbol_detect(update: Update, context: CallbackContext):
if symbols: if symbols:
# Let user know bot is working # Let user know bot is working
context.bot.send_chat_action(chat_id=chat_id, action=telegram.ChatAction.TYPING) 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): for reply in s.price_reply(symbols):
update.message.reply_text( update.message.reply_text(
text=reply, text=reply,
@ -168,6 +180,7 @@ def dividend(update: Update, context: CallbackContext):
""" """
waits for /dividend or /div command and then finds dividend info on that symbol. waits for /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 message = update.message.text
chat_id = update.message.chat_id chat_id = update.message.chat_id
@ -193,6 +206,7 @@ def news(update: Update, context: CallbackContext):
""" """
waits for /news command and then finds news info on that symbol. waits for /news command and then finds news info on that symbol.
""" """
info(f"News command ran by {update.message.chat.username}")
message = update.message.text message = update.message.text
chat_id = update.message.chat_id chat_id = update.message.chat_id
@ -215,10 +229,11 @@ def news(update: Update, context: CallbackContext):
) )
def info(update: Update, context: CallbackContext): def information(update: Update, context: CallbackContext):
""" """
waits for /info command and then finds info on that symbol. waits for /info command and then finds info on that symbol.
""" """
info(f"Information command ran by {update.message.chat.username}")
message = update.message.text message = update.message.text
chat_id = update.message.chat_id chat_id = update.message.chat_id
@ -242,6 +257,7 @@ def info(update: Update, context: CallbackContext):
def search(update: Update, context: CallbackContext): def search(update: Update, context: CallbackContext):
info(f"Search command ran by {update.message.chat.username}")
message = update.message.text.replace("/search ", "") message = update.message.text.replace("/search ", "")
chat_id = update.message.chat_id chat_id = update.message.chat_id
@ -265,6 +281,7 @@ def search(update: Update, context: CallbackContext):
def intra(update: Update, context: CallbackContext): def intra(update: Update, context: CallbackContext):
info(f"Intra command ran by {update.message.chat.username}")
# TODO: Document usage of this command. https://iexcloud.io/docs/api/#historical-prices # TODO: Document usage of this command. https://iexcloud.io/docs/api/#historical-prices
message = update.message.text message = update.message.text
@ -320,6 +337,7 @@ def intra(update: Update, context: CallbackContext):
def chart(update: Update, context: CallbackContext): def chart(update: Update, context: CallbackContext):
info(f"Chart command ran by {update.message.chat.username}")
# TODO: Document usage of this command. https://iexcloud.io/docs/api/#historical-prices # TODO: Document usage of this command. https://iexcloud.io/docs/api/#historical-prices
message = update.message.text message = update.message.text
@ -350,7 +368,7 @@ def chart(update: Update, context: CallbackContext):
context.bot.send_chat_action( context.bot.send_chat_action(
chat_id=chat_id, action=telegram.ChatAction.UPLOAD_PHOTO chat_id=chat_id, action=telegram.ChatAction.UPLOAD_PHOTO
) )
print(symbol)
buf = io.BytesIO() buf = io.BytesIO()
mpf.plot( mpf.plot(
df, df,
@ -375,6 +393,7 @@ def stat(update: Update, context: CallbackContext):
""" """
https://iexcloud.io/docs/api/#key-stats https://iexcloud.io/docs/api/#key-stats
""" """
info(f"Stat command ran by {update.message.chat.username}")
message = update.message.text message = update.message.text
chat_id = update.message.chat_id chat_id = update.message.chat_id
@ -401,6 +420,7 @@ def cap(update: Update, context: CallbackContext):
""" """
Market Cap Information Market Cap Information
""" """
info(f"Cap command ran by {update.message.chat.username}")
message = update.message.text message = update.message.text
chat_id = update.message.chat_id chat_id = update.message.chat_id
@ -427,6 +447,7 @@ def trending(update: Update, context: CallbackContext):
""" """
Trending Symbols Trending Symbols
""" """
info(f"Trending command ran by {update.message.chat.username}")
chat_id = update.message.chat_id chat_id = update.message.chat_id
@ -444,14 +465,14 @@ def inline_query(update: Update, context: CallbackContext):
Handles inline query. Handles inline query.
Does a fuzzy search on input and returns stocks that are close. Does a fuzzy search on input and returns stocks that are close.
""" """
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] matches = s.inline_search(update.inline_query.query)[:5]
symbols = " ".join([match[1].split(":")[0] for match in matches]) symbols = " ".join([match[1].split(":")[0] for match in matches])
prices = s.batch_price_reply(s.find_symbols(symbols)) prices = s.batch_price_reply(s.find_symbols(symbols))
results = [] results = []
print(update.inline_query.query)
for match, price in zip(matches, prices): for match, price in zip(matches, prices):
try: try:
results.append( results.append(
@ -464,16 +485,20 @@ def inline_query(update: Update, context: CallbackContext):
) )
) )
except TypeError: except TypeError:
logging.warning(str(match)) warning(f"{match} caused error in inline query.")
pass pass
print(match[0], "\n\n\n")
if len(results) == 5: if len(results) == 5:
update.inline_query.answer(results) update.inline_query.answer(results)
info("Inline Command was successful")
return return
update.inline_query.answer(results) update.inline_query.answer(results)
def rand_pick(update: Update, context: CallbackContext): def rand_pick(update: Update, context: CallbackContext):
info(
f"Someone is gambling! Random_pick command ran by {update.message.chat.username}"
)
update.message.reply_text( update.message.reply_text(
text=s.random_pick(), text=s.random_pick(),
@ -484,22 +509,25 @@ def rand_pick(update: Update, context: CallbackContext):
def error(update: Update, context: CallbackContext): def error(update: Update, context: CallbackContext):
"""Log Errors caused by Updates.""" """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( tb_list = traceback.format_exception(
None, context.error, context.error.__traceback__ None, context.error, context.error.__traceback__
) )
tb_string = "".join(tb_list) tb_string = "".join(tb_list)
print(tb_string)
# if update: if update:
# message = ( message = (
# f"An exception was raised while handling an update\n" 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))}" f"<pre>update = {html.escape(json.dumps(update.to_dict(), indent=2, ensure_ascii=False))}"
# "</pre>\n\n" "</pre>\n\n"
# f"<pre>context.chat_data = {html.escape(str(context.chat_data))}</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>context.user_data = {html.escape(str(context.user_data))}</pre>\n\n"
# f"<pre>{html.escape(tb_string)}</pre>" f"<pre>{html.escape(tb_string)}</pre>"
# ) )
warning(message)
else:
warning(tb_string)
update.message.reply_text( update.message.reply_text(
text="An error has occured. Please inform @MisterBiggs if the error persists." text="An error has occured. Please inform @MisterBiggs if the error persists."
) )
@ -524,7 +552,7 @@ def main():
dp.add_handler(CommandHandler("dividend", dividend)) dp.add_handler(CommandHandler("dividend", dividend))
dp.add_handler(CommandHandler("div", dividend)) dp.add_handler(CommandHandler("div", dividend))
dp.add_handler(CommandHandler("news", news)) 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("stat", stat))
dp.add_handler(CommandHandler("stats", stat)) dp.add_handler(CommandHandler("stats", stat))
dp.add_handler(CommandHandler("cap", cap)) dp.add_handler(CommandHandler("cap", cap))

View File

@ -7,7 +7,7 @@ import random
import datetime import datetime
from fuzzywuzzy import fuzz from fuzzywuzzy import fuzz
from typing import List, Tuple from logging import debug, info, warning, error, critical
from IEX_Symbol import IEX_Symbol from IEX_Symbol import IEX_Symbol
from cg_Crypto import cg_Crypto from cg_Crypto import cg_Crypto
@ -24,7 +24,7 @@ class Router:
self.stock = IEX_Symbol() self.stock = IEX_Symbol()
self.crypto = cg_Crypto() 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 """Finds stock tickers starting with a dollar sign, and cryptocurrencies with two dollar signs
in a blob of text and returns them in a list. in a blob of text and returns them in a list.
Only returns each match once. Example: Whats the price of $tsla? Only returns each match once. Example: Whats the price of $tsla?
@ -36,7 +36,7 @@ class Router:
Returns Returns
------- -------
List[str] list[str]
List of stock symbols as strings without dollar sign. List of stock symbols as strings without dollar sign.
""" """
symbols = [] symbols = []
@ -45,15 +45,15 @@ class Router:
if stock.upper() in self.stock.symbol_list["symbol"].values: if stock.upper() in self.stock.symbol_list["symbol"].values:
symbols.append(Stock(stock)) symbols.append(Stock(stock))
else: else:
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)) coins = set(re.findall(self.CRYPTO_REGEX, text))
for coin in coins: for coin in coins:
if coin.lower() in self.crypto.symbol_list["symbol"].values: if coin.lower() in self.crypto.symbol_list["symbol"].values:
symbols.append(Coin(coin.lower())) symbols.append(Coin(coin.lower()))
else: else:
print(f"{coin} is not in list of coins") info(f"{coin} is not in list of coins")
print(symbols) info(symbols)
return symbols return symbols
def status(self, bot_resp) -> str: def status(self, bot_resp) -> str:
@ -65,7 +65,7 @@ class Router:
Human readable text on status of the bot and relevant APIs Human readable text on status of the bot and relevant APIs
""" """
return f""" stats = f"""
Bot Status: Bot Status:
{bot_resp} {bot_resp}
@ -76,7 +76,11 @@ class Router:
{self.crypto.status()} {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. """Performs a fuzzy search to find stock symbols closest to a search term.
Parameters Parameters
@ -86,7 +90,7 @@ class Router:
Returns Returns
------- -------
List[tuple[str, str]] list[tuple[str, str]]
A list tuples of every stock sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name). A list tuples of every stock sorted in order of how well they match. Each tuple contains: (Symbol, Issue Name).
""" """
@ -113,7 +117,7 @@ class Router:
self.searched_symbols[search] = symbol_list self.searched_symbols[search] = symbol_list
return symbol_list return symbol_list
def inline_search(self, search: str) -> List[Tuple[str, str]]: def inline_search(self, search: str) -> list[tuple[str, str]]:
"""Searches based on the shortest symbol that contains the same string as the search. """Searches based on the shortest symbol that contains the same string as the search.
Should be very fast compared to a fuzzy search. Should be very fast compared to a fuzzy search.
@ -124,7 +128,7 @@ class Router:
Returns Returns
------- -------
List[tuple[str, str]] list[tuple[str, str]]
Each tuple contains: (Symbol, Issue Name). Each tuple contains: (Symbol, Issue Name).
""" """
@ -141,7 +145,7 @@ class Router:
self.searched_symbols[search] = symbol_list self.searched_symbols[search] = symbol_list
return symbol_list return symbol_list
def price_reply(self, symbols: list[Symbol]) -> List[str]: def price_reply(self, symbols: list[Symbol]) -> list[str]:
"""Returns current market price or after hours if its available for a given stock symbol. """Returns current market price or after hours if its available for a given stock symbol.
Parameters Parameters
@ -158,17 +162,17 @@ class Router:
replies = [] replies = []
for symbol in symbols: for symbol in symbols:
print(symbol) info(symbol)
if isinstance(symbol, Stock): if isinstance(symbol, Stock):
replies.append(self.stock.price_reply(symbol)) replies.append(self.stock.price_reply(symbol))
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
replies.append(self.crypto.price_reply(symbol)) replies.append(self.crypto.price_reply(symbol))
else: else:
print(f"{symbol} is not a Stock or Coin") info(f"{symbol} is not a Stock or Coin")
return replies return replies
def dividend_reply(self, symbols: list) -> List[str]: def dividend_reply(self, symbols: list) -> list[str]:
"""Returns the most recent, or next dividend date for a stock symbol. """Returns the most recent, or next dividend date for a stock symbol.
Parameters Parameters
@ -192,7 +196,7 @@ class Router:
return replies return replies
def news_reply(self, symbols: list) -> List[str]: def news_reply(self, symbols: list) -> list[str]:
"""Gets recent english news on stock symbols. """Gets recent english news on stock symbols.
Parameters Parameters
@ -220,12 +224,12 @@ class Router:
return replies return replies
def info_reply(self, symbols: list) -> List[str]: def info_reply(self, symbols: list) -> list[str]:
"""Gets information on stock symbols. """Gets information on stock symbols.
Parameters Parameters
---------- ----------
symbols : List[str] symbols : list[str]
List of stock symbols. List of stock symbols.
Returns Returns
@ -289,12 +293,12 @@ class Router:
print(f"{symbol} is not a Stock or Coin") print(f"{symbol} is not a Stock or Coin")
return pd.DataFrame() return pd.DataFrame()
def stat_reply(self, symbols: List[Symbol]) -> List[str]: def stat_reply(self, symbols: list[Symbol]) -> list[str]:
"""Gets key statistics for each symbol in the list """Gets key statistics for each symbol in the list
Parameters Parameters
---------- ----------
symbols : List[str] symbols : list[str]
List of stock symbols List of stock symbols
Returns Returns
@ -314,12 +318,12 @@ class Router:
return replies return replies
def cap_reply(self, symbols: List[Symbol]) -> List[str]: def cap_reply(self, symbols: list[Symbol]) -> list[str]:
"""Gets market cap for each symbol in the list """Gets market cap for each symbol in the list
Parameters Parameters
---------- ----------
symbols : List[str] symbols : list[str]
List of stock symbols List of stock symbols
Returns Returns
@ -375,7 +379,7 @@ class Router:
return f"{choice}\nBuy and hold until: {hold}" return f"{choice}\nBuy and hold until: {hold}"
def batch_price_reply(self, symbols: list[Symbol]) -> List[str]: def batch_price_reply(self, symbols: list[Symbol]) -> list[str]:
"""Returns current market price or after hours if its available for a given stock symbol. """Returns current market price or after hours if its available for a given stock symbol.
Parameters Parameters