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

Merge branch 'canary' into 'master'

May 2021 Update

Closes #66, #62, #60, and #55

See merge request simple-stock-bots/simple-telegram-stock-bot!22
This commit is contained in:
Anson Biggs 2021-05-27 05:31:13 +00:00
commit 14c50deb82
5 changed files with 161 additions and 94 deletions

View File

@ -55,10 +55,12 @@ class IEX_Symbol:
) -> Optional[Tuple[pd.DataFrame, datetime]]: ) -> Optional[Tuple[pd.DataFrame, datetime]]:
reg_symbols = r.get( reg_symbols = r.get(
f"https://cloud.iexapis.com/stable/ref-data/symbols?token={self.IEX_TOKEN}" f"https://cloud.iexapis.com/stable/ref-data/symbols?token={self.IEX_TOKEN}",
timeout=5,
).json() ).json()
otc_symbols = r.get( otc_symbols = r.get(
f"https://cloud.iexapis.com/stable/ref-data/otc/symbols?token={self.IEX_TOKEN}" f"https://cloud.iexapis.com/stable/ref-data/otc/symbols?token={self.IEX_TOKEN}",
timeout=5,
).json() ).json()
reg = pd.DataFrame(data=reg_symbols) reg = pd.DataFrame(data=reg_symbols)
@ -69,8 +71,9 @@ class IEX_Symbol:
symbols["description"] = "$" + symbols["symbol"] + ": " + symbols["name"] symbols["description"] = "$" + symbols["symbol"] + ": " + symbols["name"]
symbols["id"] = symbols["symbol"] symbols["id"] = symbols["symbol"]
symbols["type_id"] = "$" + symbols["symbol"].str.lower()
symbols = symbols[["id", "symbol", "name", "description"]] symbols = symbols[["id", "symbol", "name", "description", "type_id"]]
self.symbol_list = symbols self.symbol_list = symbols
if return_df: if return_df:
return symbols, datetime.now() return symbols, datetime.now()
@ -83,7 +86,10 @@ class IEX_Symbol:
str str
Human readable text on status of IEX API Human readable text on status of IEX API
""" """
resp = r.get("https://pjmps0c34hp7.statuspage.io/api/v2/status.json") resp = r.get(
"https://pjmps0c34hp7.statuspage.io/api/v2/status.json",
timeout=5,
)
if resp.status_code == 200: if resp.status_code == 200:
status = resp.json()["status"] status = resp.json()["status"]
@ -155,7 +161,10 @@ class IEX_Symbol:
IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol.id}/quote?token={self.IEX_TOKEN}" IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol.id}/quote?token={self.IEX_TOKEN}"
response = r.get(IEXurl) response = r.get(
IEXurl,
timeout=5,
)
if response.status_code == 200: if response.status_code == 200:
IEXData = response.json() IEXData = response.json()
@ -226,7 +235,10 @@ class IEX_Symbol:
return "OTC stocks do not currently support any commands." return "OTC stocks do not currently support any commands."
IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/dividends/next?token={self.IEX_TOKEN}" IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/dividends/next?token={self.IEX_TOKEN}"
response = r.get(IEXurl) response = r.get(
IEXurl,
timeout=5,
)
if response.status_code == 200 and response.json(): if response.status_code == 200 and response.json():
IEXData = response.json()[0] IEXData = response.json()[0]
keys = ( keys = (
@ -288,7 +300,10 @@ class IEX_Symbol:
return "OTC stocks do not currently support any commands." return "OTC stocks do not currently support any commands."
IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/news/last/15?token={self.IEX_TOKEN}" IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/news/last/15?token={self.IEX_TOKEN}"
response = r.get(IEXurl) response = r.get(
IEXurl,
timeout=5,
)
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
if data: if data:
@ -324,7 +339,10 @@ class IEX_Symbol:
return "OTC stocks do not currently support any commands." return "OTC stocks do not currently support any commands."
IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/company?token={self.IEX_TOKEN}" IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/company?token={self.IEX_TOKEN}"
response = r.get(IEXurl) response = r.get(
IEXurl,
timeout=5,
)
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
@ -352,7 +370,10 @@ class IEX_Symbol:
return "OTC stocks do not currently support any commands." return "OTC stocks do not currently support any commands."
IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/stats?token={self.IEX_TOKEN}" IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/stats?token={self.IEX_TOKEN}"
response = r.get(IEXurl) response = r.get(
IEXurl,
timeout=5,
)
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
@ -362,7 +383,7 @@ class IEX_Symbol:
if "companyName" in data: if "companyName" in data:
m += f"Company Name: {data['companyName']}\n" m += f"Company Name: {data['companyName']}\n"
if "marketcap" in data: if "marketcap" in data:
m += f"Market Cap: {data['marketcap']:,}\n" m += f"Market Cap: ${data['marketcap']:,}\n"
if "week52high" in data: if "week52high" in data:
m += f"52 Week (high-low): {data['week52high']:,} " m += f"52 Week (high-low): {data['week52high']:,} "
if "week52low" in data: if "week52low" in data:
@ -399,7 +420,10 @@ class IEX_Symbol:
return pd.DataFrame() return pd.DataFrame()
IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/intraday-prices?token={self.IEX_TOKEN}" IEXurl = f"https://cloud.iexapis.com/stable/stock/{symbol}/intraday-prices?token={self.IEX_TOKEN}"
response = r.get(IEXurl) response = r.get(
IEXurl,
timeout=5,
)
if response.status_code == 200: if response.status_code == 200:
df = pd.DataFrame(response.json()) df = pd.DataFrame(response.json())
df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"]) df.dropna(inplace=True, subset=["date", "minute", "high", "low", "volume"])
@ -437,7 +461,8 @@ class IEX_Symbol:
pass pass
response = r.get( response = r.get(
f"https://cloud.iexapis.com/stable/stock/{symbol}/chart/1mm?token={self.IEX_TOKEN}&chartInterval=3&includeToday=false" f"https://cloud.iexapis.com/stable/stock/{symbol}/chart/1mm?token={self.IEX_TOKEN}&chartInterval=3&includeToday=false",
timeout=5,
) )
if response.status_code == 200: if response.status_code == 200:
@ -460,7 +485,8 @@ class IEX_Symbol:
""" """
stocks = r.get( stocks = r.get(
f"https://cloud.iexapis.com/stable/stock/market/list/mostactive?token={self.IEX_TOKEN}" f"https://cloud.iexapis.com/stable/stock/market/list/mostactive?token={self.IEX_TOKEN}",
timeout=5,
).json() ).json()
return [f"${s['symbol']}: {s['companyName']}" for s in stocks] return [f"${s['symbol']}: {s['companyName']}" for s in stocks]

87
bot.py
View File

@ -98,17 +98,9 @@ def donate(update: Update, context: CallbackContext):
parse_mode=telegram.ParseMode.MARKDOWN, parse_mode=telegram.ParseMode.MARKDOWN,
disable_notification=True, disable_notification=True,
) )
return amount = 1
else: else:
amount = update.message.text.replace("/donate", "").replace("$", "").strip() amount = update.message.text.replace("/donate", "").replace("$", "").strip()
title = "Simple Stock Bot Donation"
description = f"Simple Stock Bot Donation of ${amount}"
payload = "simple-stock-bot"
provider_token = STRIPE_TOKEN
start_parameter = str(chat_id)
print(start_parameter)
currency = "USD"
try: try:
price = int(float(amount) * 100) price = int(float(amount) * 100)
@ -117,32 +109,38 @@ def donate(update: Update, context: CallbackContext):
return return
print(price) print(price)
prices = [LabeledPrice("Donation:", price)]
context.bot.send_invoice( context.bot.send_invoice(
chat_id, chat_id=chat_id,
title, title="Simple Stock Bot Donation",
description, description=f"Simple Stock Bot Donation of ${amount}",
payload, payload=f"simple-stock-bot-{chat_id}",
provider_token, provider_token=STRIPE_TOKEN,
start_parameter, currency="USD",
currency, prices=[LabeledPrice("Donation:", price)],
prices, start_parameter="",
# suggested_tip_amounts=[100, 500, 1000, 2000],
photo_url="https://simple-stock-bots.gitlab.io/site/img/Telegram.png",
photo_width=500,
photo_height=500,
) )
def precheckout_callback(update: Update, context: CallbackContext): def precheckout_callback(update: Update, context: CallbackContext):
query = update.pre_checkout_query query = update.pre_checkout_query
if query.invoice_payload == "simple-stock-bot": query.answer(ok=True)
# answer False pre_checkout_query # I dont think I need to check since its only donations.
query.answer(ok=True) # if query.invoice_payload == "simple-stock-bot":
else: # # answer False pre_checkout_query
query.answer(ok=False, error_message="Something went wrong...") # query.answer(ok=True)
# else:
# query.answer(ok=False, error_message="Something went wrong...")
def successful_payment_callback(update: Update, context: CallbackContext): def successful_payment_callback(update: Update, context: CallbackContext):
update.message.reply_text("Thank you for your donation!") update.message.reply_text(
"Thank you for your donation! It goes a long way to keeping the bot free!"
)
def symbol_detect(update: Update, context: CallbackContext): def symbol_detect(update: Update, context: CallbackContext):
@ -307,16 +305,15 @@ def intra(update: Update, context: CallbackContext):
title=f"\n{symbol.name}", title=f"\n{symbol.name}",
volume="volume" in df.keys(), volume="volume" in df.keys(),
style="yahoo", style="yahoo",
mav=20,
savefig=dict(fname=buf, dpi=400, bbox_inches="tight"), savefig=dict(fname=buf, dpi=400, bbox_inches="tight"),
) )
buf.seek(0) buf.seek(0)
update.message.reply_photo( update.message.reply_photo(
photo=buf, photo=buf,
caption=f"\nIntraday chart for {symbol.name} from {df.first_valid_index().strftime('%I:%M')} to" caption=f"\nIntraday chart for {symbol.name} from {df.first_valid_index().strftime('%d %b at %H:%M')} to"
+ f" {df.last_valid_index().strftime('%I:%M')} ET on" + f" {df.last_valid_index().strftime('%d %b at %H:%M')}"
+ f" {datetime.date.today().strftime('%d, %b %Y')}\n\n{s.price_reply([symbol])[0]}", + f"\n\n{s.price_reply([symbol])[0]}",
parse_mode=telegram.ParseMode.MARKDOWN, parse_mode=telegram.ParseMode.MARKDOWN,
disable_notification=True, disable_notification=True,
) )
@ -422,12 +419,11 @@ def inline_query(update: Update, context: CallbackContext):
Does a fuzzy search on input and returns stocks that are close. Does a fuzzy search on input and returns stocks that are close.
""" """
matches = s.search_symbols(update.inline_query.query)[:] matches = s.inline_search(update.inline_query.query)[:5]
symbols = " ".join([match[1].split(":")[0] for match in matches]) 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))
# print(len(matches), len(prices))
# print(prices)
results = [] results = []
print(update.inline_query.query) print(update.inline_query.query)
for match, price in zip(matches, prices): for match, price in zip(matches, prices):
@ -469,19 +465,22 @@ def error(update: Update, context: CallbackContext):
) )
tb_string = "".join(tb_list) tb_string = "".join(tb_list)
print(tb_string) 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>"
) # )
update.message.reply_text(
text="An error has occured. Please inform @MisterBiggs if the error persists."
)
# Finally, send the message # Finally, send the message
# update.message.reply_text(text=message, parse_mode=telegram.ParseMode.HTML) # update.message.reply_text(text=message, parse_mode=telegram.ParseMode.HTML)
# update.message.reply_text(text="Please inform the bot admin of this issue.") # update.message.reply_text(text="Please inform the bot admin of this issue.")
def main(): def main():

View File

@ -44,11 +44,17 @@ class cg_Crypto:
self, return_df=False self, return_df=False
) -> Optional[Tuple[pd.DataFrame, datetime]]: ) -> Optional[Tuple[pd.DataFrame, datetime]]:
raw_symbols = r.get("https://api.coingecko.com/api/v3/coins/list").json() raw_symbols = r.get(
"https://api.coingecko.com/api/v3/coins/list",
timeout=5,
).json()
symbols = pd.DataFrame(data=raw_symbols) symbols = pd.DataFrame(data=raw_symbols)
symbols["description"] = "$$" + symbols["symbol"] + ": " + symbols["name"] symbols["description"] = (
"$$" + symbols["symbol"].str.upper() + ": " + symbols["name"]
)
symbols = symbols[["id", "symbol", "name", "description"]] symbols = symbols[["id", "symbol", "name", "description"]]
symbols["type_id"] = "$$" + symbols["id"]
self.symbol_list = symbols self.symbol_list = symbols
if return_df: if return_df:
@ -62,7 +68,10 @@ class cg_Crypto:
str str
Human readable text on status of CoinGecko API Human readable text on status of CoinGecko API
""" """
status = r.get("https://api.coingecko.com/api/v3/ping") status = r.get(
"https://api.coingecko.com/api/v3/ping",
timeout=5,
)
if status.status_code == 200: if status.status_code == 200:
return f"CoinGecko API responded that it was OK in {status.elapsed.total_seconds()} Seconds." return f"CoinGecko API responded that it was OK in {status.elapsed.total_seconds()} Seconds."
@ -124,7 +133,8 @@ class cg_Crypto:
""" """
response = r.get( response = r.get(
f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false" f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false",
timeout=5,
) )
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
@ -167,7 +177,8 @@ class cg_Crypto:
Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
""" """
response = r.get( response = r.get(
f"https://api.coingecko.com/api/v3/coins/{symbol.id}/ohlc?vs_currency=usd&days=1" f"https://api.coingecko.com/api/v3/coins/{symbol.id}/ohlc?vs_currency=usd&days=1",
timeout=5,
) )
if response.status_code == 200: if response.status_code == 200:
df = pd.DataFrame( df = pd.DataFrame(
@ -194,7 +205,8 @@ class cg_Crypto:
Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame. Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
""" """
response = r.get( response = r.get(
f"https://api.coingecko.com/api/v3/coins/{symbol.id}/ohlc?vs_currency=usd&days=30" f"https://api.coingecko.com/api/v3/coins/{symbol.id}/ohlc?vs_currency=usd&days=30",
timeout=5,
) )
if response.status_code == 200: if response.status_code == 200:
@ -221,14 +233,16 @@ class cg_Crypto:
Each symbol passed in is a key with its value being a human readable formatted string of the symbols statistics. Each symbol passed in is a key with its value being a human readable formatted string of the symbols statistics.
""" """
response = r.get( response = r.get(
f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false" f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false",
timeout=5,
) )
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
return f""" return f"""
[{data['name']}]({data['links']['homepage'][0]}) Statistics: [{data['name']}]({data['links']['homepage'][0]}) Statistics:
Maket Cap Ranking: {data.get('market_cap_rank',"Not Available")} Market Cap: ${data['market_data']['market_cap'][self.vs_currency]:,}
Market Cap Ranking: {data.get('market_cap_rank',"Not Available")}
CoinGecko Scores: CoinGecko Scores:
Overall: {data.get('coingecko_score','Not Available')} Overall: {data.get('coingecko_score','Not Available')}
Development: {data.get('developer_score','Not Available')} Development: {data.get('developer_score','Not Available')}
@ -253,7 +267,8 @@ class cg_Crypto:
""" """
response = r.get( response = r.get(
f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false" f"https://api.coingecko.com/api/v3/coins/{symbol.id}?localization=false",
timeout=5,
) )
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
@ -273,9 +288,10 @@ class cg_Crypto:
list of $$ID: NAME list of $$ID: NAME
""" """
coins = r.get("https://api.coingecko.com/api/v3/search/trending").json()[ coins = r.get(
"coins" "https://api.coingecko.com/api/v3/search/trending",
] timeout=5,
).json()["coins"]
return [f"$${c['item']['symbol'].upper()}: {c['item']['name']}" for c in coins] return [f"$${c['item']['symbol'].upper()}: {c['item']['name']}" for c in coins]
@ -283,23 +299,20 @@ class cg_Crypto:
query = ",".join([c.id for c in coins]) query = ",".join([c.id for c in coins])
prices = r.get( prices = r.get(
f"https://api.coingecko.com/api/v3/simple/price?ids={query}&vs_currencies=usd&include_24hr_change=true" f"https://api.coingecko.com/api/v3/simple/price?ids={query}&vs_currencies=usd&include_24hr_change=true",
timeout=5,
).json() ).json()
replies = [] replies = []
for name, val in prices.items(): for coin in coins:
if price := val.get("usd"): if coin.id in prices:
price = val.get("usd") p = prices[coin.id]
else:
replies.append(f"{name} price data unavailable.")
break
change = 0 if p.get("usd_24h_change") is None:
if val.get("usd_24h_change") is not None: p["usd_24h_change"] = 0
change = val.get("usd_24h_change")
replies.append( replies.append(
f"{name}: ${price:,} and has moved {change:.2f}% in the past 24 hours." f"{coin.name}: ${p.get('usd',0):,} and has moved {p.get('usd_24h_change',0.0):.2f}% in the past 24 hours."
) )
return replies return replies

View File

@ -1,4 +1,4 @@
python-telegram-bot==13.2 python-telegram-bot==13.5
requests==2.25.1 requests==2.25.1
pandas==1.2.1 pandas==1.2.1
fuzzywuzzy==0.18.0 fuzzywuzzy==0.18.0

View File

@ -100,13 +100,41 @@ class Router:
) )
df.sort_values(by="Match", ascending=False, inplace=True) df.sort_values(by="Match", ascending=False, inplace=True)
if df["Match"].head().sum() < 300: # if df["Match"].head().sum() < 300:
df["Match"] = df.apply( # df["Match"] = df.apply(
lambda x: fuzz.partial_ratio(search, x["name"].lower()), # lambda x: fuzz.partial_ratio(search, x["name"].lower()),
axis=1, # axis=1,
) # )
df.sort_values(by="Match", ascending=False, inplace=True) # df.sort_values(by="Match", ascending=False, inplace=True)
symbols = df.head(20)
symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"])))
self.searched_symbols[search] = symbol_list
return symbol_list
def inline_search(self, search: str) -> List[Tuple[str, str]]:
"""Searches based on the shortest symbol that contains the same string as the search.
Should be very fast compared to a fuzzy search.
Parameters
----------
search : str
String used to match against symbols.
Returns
-------
List[tuple[str, str]]
Each tuple contains: (Symbol, Issue Name).
"""
df = pd.concat([self.stock.symbol_list, self.crypto.symbol_list])
search = search.lower()
df = df[df["type_id"].str.contains(search, regex=False)].sort_values(
by="type_id", key=lambda x: x.str.len()
)
symbols = df.head(20) symbols = df.head(20)
symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"]))) symbol_list = list(zip(list(symbols["symbol"]), list(symbols["description"])))
@ -184,7 +212,9 @@ class Router:
replies.append(self.stock.news_reply(symbol)) replies.append(self.stock.news_reply(symbol))
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
# replies.append(self.crypto.news_reply(symbol)) # replies.append(self.crypto.news_reply(symbol))
replies.append("News is not yet supported for cryptocurrencies.") replies.append(
"News is not yet supported for cryptocurrencies. If you have any suggestions for news sources please contatct @MisterBiggs"
)
else: else:
print(f"{symbol} is not a Stock or Coin") print(f"{symbol} is not a Stock or Coin")
@ -347,11 +377,10 @@ class Router:
print(f"{symbol} is not a Stock or Coin") print(f"{symbol} is not a Stock or Coin")
if stocks: if stocks:
for ( # IEX batch endpoint doesnt seem to be working right now
stock for stock in stocks:
) in stocks: # IEX batch endpoint doesnt seem to be working right now
replies.append(self.stock.price_reply(stock)) replies.append(self.stock.price_reply(stock))
if coins: if coins:
replies.append(self.crypto.batch_price(coins)) replies = replies + self.crypto.batch_price(coins)
return replies return replies