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:
commit
88a9b3aa63
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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
6
bot.py
@ -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))
|
||||||
|
19
cg_Crypto.py
19
cg_Crypto.py
@ -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]:
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user