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

Merge branch 'canary' into 'master'

Trending Update

Closes #73 and #74

See merge request simple-stock-bots/simple-telegram-stock-bot!29
This commit is contained in:
Anson Biggs 2021-09-01 03:23:01 +00:00
commit 88a9b3aa63
7 changed files with 1447 additions and 1340 deletions

View File

@ -5,7 +5,7 @@
// Sets the run context to one level up instead of the .devcontainer folder. // Sets the run context to one level up instead of the .devcontainer folder.
"context": "..", "context": "..",
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename. // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
"dockerFile": "../DockerDev", "dockerFile": "Dockerfile",
// Set *default* container specific settings.json values on container create. // Set *default* container specific settings.json values on container create.
"settings": {}, "settings": {},
// Add the IDs of extensions you want installed when the container is created. // Add the IDs of extensions you want installed when the container is created.

View File

@ -25,6 +25,7 @@ class IEX_Symbol:
searched_symbols = {} searched_symbols = {}
otc_list = [] otc_list = []
charts = {} charts = {}
trending_cache = None
def __init__(self) -> None: def __init__(self) -> None:
"""Creates a Symbol Object """Creates a Symbol Object
@ -35,7 +36,7 @@ class IEX_Symbol:
IEX API Token IEX API Token
""" """
try: try:
self.IEX_TOKEN = os.environ["IEX"] self.IEX_TOKEN = "pk_3c39d940736e47dabfdd47eb689a65be"
except KeyError: except KeyError:
self.IEX_TOKEN = "" self.IEX_TOKEN = ""
warning( warning(
@ -484,6 +485,23 @@ class IEX_Symbol:
return pd.DataFrame() return pd.DataFrame()
def spark_reply(self, symbol: Stock) -> str:
quote = self.get(f"/stock/{symbol.id}/quote")
open_change = quote.get("changePercent", 0)
after_change = quote.get("extendedChangePercent", 0)
change = 0
if open_change:
change = change + open_change
if after_change:
change = change + after_change
change = change * 100
return f"`{symbol.tag}`: {quote['companyName']}, {change:.2f}%"
def trending(self) -> list[str]: def trending(self) -> list[str]:
"""Gets current coins trending on IEX. Only returns when market is open. """Gets current coins trending on IEX. Only returns when market is open.
@ -494,9 +512,9 @@ class IEX_Symbol:
""" """
if data := self.get(f"/stock/market/list/mostactive"): if data := self.get(f"/stock/market/list/mostactive"):
return [ self.trending_cache = [
f"`${s['symbol']}`: {s['companyName']}, {100*s['changePercent']:.2f}%" f"`${s['symbol']}`: {s['companyName']}, {100*s['changePercent']:.2f}%"
for s in data for s in data
] ]
else:
return ["Trending Stocks Currently Unavailable."] return self.trending_cache

View File

@ -8,6 +8,7 @@ class Symbol:
symbol: What the user calls it. ie tsla or btc symbol: What the user calls it. ie tsla or btc
id: What the api expects. ie tsla or bitcoin id: What the api expects. ie tsla or bitcoin
name: Human readable. ie Tesla or Bitcoin name: Human readable. ie Tesla or Bitcoin
tag: Uppercase tag to call the symbol. ie $TSLA or $$BTC
""" """
currency = "usd" currency = "usd"
@ -17,6 +18,7 @@ class Symbol:
self.symbol = symbol self.symbol = symbol
self.id = symbol self.id = symbol
self.name = symbol self.name = symbol
self.tag = "$" + symbol
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<{self.__class__.__name__} instance of {self.id} at {id(self)}>" return f"<{self.__class__.__name__} instance of {self.id} at {id(self)}>"
@ -32,6 +34,7 @@ class Stock(Symbol):
self.symbol = symbol self.symbol = symbol
self.id = symbol self.id = symbol
self.name = "$" + symbol.upper() self.name = "$" + symbol.upper()
self.tag = "$" + symbol.upper()
# Used by Coin to change symbols for ids # Used by Coin to change symbols for ids
@ -44,6 +47,7 @@ class Coin(Symbol):
@functools.cache @functools.cache
def __init__(self, symbol: str) -> None: def __init__(self, symbol: str) -> None:
self.symbol = symbol self.symbol = symbol
self.tag = "$$" + symbol.upper()
self.get_data() self.get_data()
def get_data(self) -> None: def get_data(self) -> None:

6
bot.py
View File

@ -116,12 +116,12 @@ 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
info(f"Donation amount: {price}") info(f"Donation amount: {price} by {update.message.chat.username}")
context.bot.send_invoice( context.bot.send_invoice(
chat_id=chat_id, chat_id=chat_id,
title="Simple Stock Bot Donation", title="Simple Stock Bot Donation",
description=f"Simple Stock Bot Donation of ${amount}", description=f"Simple Stock Bot Donation of ${amount} by {update.message.chat.username}",
payload=f"simple-stock-bot-{chat_id}", payload=f"simple-stock-bot-{chat_id}",
provider_token=STRIPE_TOKEN, provider_token=STRIPE_TOKEN,
currency="USD", currency="USD",
@ -560,7 +560,7 @@ def main():
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))
dp.add_handler(CommandHandler("trending", trending)) dp.add_handler(CommandHandler("trending", trending, run_async=True))
dp.add_handler(CommandHandler("search", search)) dp.add_handler(CommandHandler("search", search))
dp.add_handler(CommandHandler("random", rand_pick)) dp.add_handler(CommandHandler("random", rand_pick))
dp.add_handler(CommandHandler("donate", donate)) dp.add_handler(CommandHandler("donate", donate))

View File

@ -3,6 +3,7 @@
import logging import logging
from datetime import datetime from datetime import datetime
from logging import critical, debug, error, info, warning
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
import pandas as pd import pandas as pd
@ -22,6 +23,7 @@ class cg_Crypto:
vs_currency = "usd" # simple/supported_vs_currencies for list of options vs_currency = "usd" # simple/supported_vs_currencies for list of options
searched_symbols = {} searched_symbols = {}
trending_cache = None
def __init__(self) -> None: def __init__(self) -> None:
"""Creates a Symbol Object """Creates a Symbol Object
@ -293,7 +295,7 @@ class cg_Crypto:
"include_market_cap": "true", "include_market_cap": "true",
}, },
): ):
print(resp) debug(resp)
try: try:
data = resp[coin.id] data = resp[coin.id]
@ -336,6 +338,18 @@ class cg_Crypto:
return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist." return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist."
def spark_reply(self, symbol: Coin) -> str:
change = self.get(
f"/simple/price",
params={
"ids": symbol.id,
"vs_currencies": self.vs_currency,
"include_24hr_change": "true",
},
)[symbol.id]["usd_24h_change"]
return f"`{symbol.tag}`: {symbol.name}, {change:.2f}%"
def trending(self) -> list[str]: def trending(self) -> list[str]:
"""Gets current coins trending on coingecko """Gets current coins trending on coingecko
@ -368,8 +382,9 @@ class cg_Crypto:
except Exception as e: except Exception as e:
logging.warning(e) logging.warning(e)
trending = ["Trending Coins Currently Unavailable."] return self.trending_cache
self.trending_cache = trending
return trending return trending
def batch_price(self, coins: list[Coin]) -> list[str]: def batch_price(self, coins: list[Coin]) -> list[str]:

View File

@ -7,6 +7,7 @@ import re
from logging import critical, debug, error, info, warning from logging import critical, debug, error, info, warning
import pandas as pd import pandas as pd
import schedule
from fuzzywuzzy import fuzz from fuzzywuzzy import fuzz
from cg_Crypto import cg_Crypto from cg_Crypto import cg_Crypto
@ -18,11 +19,26 @@ class Router:
STOCK_REGEX = "(?:^|[^\\$])\\$([a-zA-Z.]{1,6})" STOCK_REGEX = "(?:^|[^\\$])\\$([a-zA-Z.]{1,6})"
CRYPTO_REGEX = "[$]{2}([a-zA-Z]{1,20})" CRYPTO_REGEX = "[$]{2}([a-zA-Z]{1,20})"
searched_symbols = {} searched_symbols = {}
trending_count = {}
def __init__(self): def __init__(self):
self.stock = IEX_Symbol() self.stock = IEX_Symbol()
self.crypto = cg_Crypto() self.crypto = cg_Crypto()
schedule.every().hour.do(self.trending_decay)
def trending_decay(self, decay=0.5):
"""Decays the value of each trending stock by a multiplier"""
info("Decaying trending symbols.")
if self.trending_count:
for key in self.trending_count.keys():
if self.trending_count[key] < 0.01:
# This just makes sure were not keeping around keys that havent been called in a very long time.
self.trending_count.pop(key, None)
else:
self.trending_count[key] = self.trending_count[key] * decay
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.
@ -34,9 +50,11 @@ class Router:
Returns Returns
------- -------
list[str] list[Symbol]
List of stock symbols as strings without dollar sign. List of stock symbols as Symbol objects
""" """
schedule.run_pending()
symbols = [] symbols = []
stocks = set(re.findall(self.STOCK_REGEX, text)) stocks = set(re.findall(self.STOCK_REGEX, text))
for stock in stocks: for stock in stocks:
@ -54,6 +72,10 @@ class Router:
if symbols: if symbols:
info(symbols) info(symbols)
for symbol in symbols:
self.trending_count[symbol.tag] = (
self.trending_count.get(symbol.tag, 0) + 1
)
return symbols return symbols
@ -195,7 +217,7 @@ class Router:
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
replies.append("Cryptocurrencies do no have Dividends.") replies.append("Cryptocurrencies do no have Dividends.")
else: else:
print(f"{symbol} is not a Stock or Coin") debug(f"{symbol} is not a Stock or Coin")
return replies return replies
@ -224,7 +246,7 @@ class Router:
"News is not yet supported for cryptocurrencies. If you have any suggestions for news sources please contatct @MisterBiggs" "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") debug(f"{symbol} is not a Stock or Coin")
return replies return replies
@ -250,7 +272,7 @@ class Router:
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
replies.append(self.crypto.info_reply(symbol)) replies.append(self.crypto.info_reply(symbol))
else: else:
print(f"{symbol} is not a Stock or Coin") debug(f"{symbol} is not a Stock or Coin")
return replies return replies
@ -274,7 +296,7 @@ class Router:
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
return self.crypto.intra_reply(symbol) return self.crypto.intra_reply(symbol)
else: else:
print(f"{symbol} is not a Stock or Coin") debug(f"{symbol} is not a Stock or Coin")
return pd.DataFrame() return pd.DataFrame()
def chart_reply(self, symbol: Symbol) -> pd.DataFrame: def chart_reply(self, symbol: Symbol) -> pd.DataFrame:
@ -297,7 +319,7 @@ class Router:
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
return self.crypto.chart_reply(symbol) return self.crypto.chart_reply(symbol)
else: else:
print(f"{symbol} is not a Stock or Coin") debug(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]:
@ -322,7 +344,7 @@ class Router:
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
replies.append(self.crypto.stat_reply(symbol)) replies.append(self.crypto.stat_reply(symbol))
else: else:
print(f"{symbol} is not a Stock or Coin") debug(f"{symbol} is not a Stock or Coin")
return replies return replies
@ -348,7 +370,32 @@ class Router:
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
replies.append(self.crypto.cap_reply(symbol)) replies.append(self.crypto.cap_reply(symbol))
else: else:
print(f"{symbol} is not a Stock or Coin") debug(f"{symbol} is not a Stock or Coin")
return replies
def spark_reply(self, symbols: list[Symbol]) -> list[str]:
"""Gets change for each symbol and returns it in a compact format
Parameters
----------
symbols : list[str]
List of stock symbols
Returns
-------
list[str]
List of human readable strings.
"""
replies = []
for symbol in symbols:
if isinstance(symbol, Stock):
replies.append(self.stock.spark_reply(symbol))
elif isinstance(symbol, Coin):
replies.append(self.crypto.spark_reply(symbol))
else:
debug(f"{symbol} is not a Stock or Coin")
return replies return replies
@ -364,17 +411,40 @@ class Router:
stocks = self.stock.trending() stocks = self.stock.trending()
coins = self.crypto.trending() coins = self.crypto.trending()
reply = "Trending Stocks:\n" reply = ""
reply += "-" * len("Trending Stocks:") + "\n"
if self.trending_count:
reply += "🔥Trending on the Stock Bot:\n`"
reply += "" * len("Trending on the Stock Bot:") + "`\n"
sorted_trending = [
s[0]
for s in sorted(self.trending_count.items(), key=lambda item: item[1])
][::-1][0:5]
for t in sorted_trending:
reply += self.spark_reply(self.find_symbols(t))[0] + "\n"
if stocks:
reply += "\n\n💵Trending Stocks:\n`"
reply += "" * len("Trending Stocks:") + "`\n"
for stock in stocks: for stock in stocks:
reply += stock + "\n" reply += stock + "\n"
reply += "\n\nTrending Crypto:\n" if coins:
reply += "-" * len("Trending Crypto:") + "\n" reply += "\n\n🦎Trending Crypto:\n`"
reply += "" * len("Trending Crypto:") + "`\n"
for coin in coins: for coin in coins:
reply += coin + "\n" reply += coin + "\n"
if "`$GME" in reply:
reply = reply.replace("🔥", "🦍")
if reply:
return reply return reply
else:
warning("Failed to collect trending data.")
return "Trending data is not currently available."
def random_pick(self) -> str: def random_pick(self) -> str:
@ -412,7 +482,7 @@ class Router:
elif isinstance(symbol, Coin): elif isinstance(symbol, Coin):
coins.append(symbol) coins.append(symbol)
else: else:
print(f"{symbol} is not a Stock or Coin") debug(f"{symbol} is not a Stock or Coin")
if stocks: if stocks:
# IEX batch endpoint doesnt seem to be working right now # IEX batch endpoint doesnt seem to be working right now