mirror of
https://gitlab.com/simple-stock-bots/simple-stock-bot.git
synced 2025-06-16 07:16:40 +00:00
formatting
This commit is contained in:
parent
0c71193194
commit
1fe7fe8c9c
@ -1,362 +1,371 @@
|
|||||||
import datetime as dt
|
import datetime as dt
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
import humanize
|
import humanize
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytz
|
import pytz
|
||||||
import requests as r
|
import requests as r
|
||||||
import schedule
|
import schedule
|
||||||
|
|
||||||
from common.Symbol import Stock
|
|
||||||
|
from common.Symbol import Stock
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
class MarketData:
|
|
||||||
"""
|
class MarketData:
|
||||||
Functions for finding stock market information about symbols from MarkData.app
|
"""
|
||||||
"""
|
Functions for finding stock market information about symbols from MarkData.app
|
||||||
|
"""
|
||||||
SYMBOL_REGEX = "[$]([a-zA-Z]{1,4})"
|
|
||||||
|
SYMBOL_REGEX = "[$]([a-zA-Z]{1,4})"
|
||||||
symbol_list: Dict[str, Dict] = {}
|
|
||||||
charts: Dict[Stock, pd.DataFrame] = {}
|
symbol_list: Dict[str, Dict] = {}
|
||||||
|
charts: Dict[Stock, pd.DataFrame] = {}
|
||||||
openTime = dt.time(hour=9, minute=30, second=0)
|
|
||||||
marketTimeZone = pytz.timezone("US/Eastern")
|
openTime = dt.time(hour=9, minute=30, second=0)
|
||||||
|
marketTimeZone = pytz.timezone("US/Eastern")
|
||||||
def __init__(self) -> None:
|
|
||||||
"""Creates a Symbol Object
|
def __init__(self) -> None:
|
||||||
|
"""Creates a Symbol Object
|
||||||
Parameters
|
|
||||||
----------
|
Parameters
|
||||||
MARKETDATA_TOKEN : str
|
----------
|
||||||
MarketData.app API Token
|
MARKETDATA_TOKEN : str
|
||||||
"""
|
MarketData.app API Token
|
||||||
|
"""
|
||||||
try:
|
|
||||||
self.MARKETDATA_TOKEN = os.environ["MARKETDATA"]
|
try:
|
||||||
|
self.MARKETDATA_TOKEN = os.environ["MARKETDATA"]
|
||||||
if self.MARKETDATA_TOKEN == "TOKEN":
|
|
||||||
self.MARKETDATA_TOKEN = ""
|
if self.MARKETDATA_TOKEN == "TOKEN":
|
||||||
except KeyError:
|
self.MARKETDATA_TOKEN = ""
|
||||||
self.MARKETDATA_TOKEN = ""
|
except KeyError:
|
||||||
log.warning("Starting without an MarketData.app Token will not allow you to get market data!")
|
self.MARKETDATA_TOKEN = ""
|
||||||
log.warning("Use this affiliate link so that the bot can stay free:")
|
log.warning(
|
||||||
log.warning("https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=repo")
|
"Starting without an MarketData.app Token will not allow you to get market data!"
|
||||||
|
)
|
||||||
if self.MARKETDATA_TOKEN != "":
|
log.warning("Use this affiliate link so that the bot can stay free:")
|
||||||
schedule.every().day.do(self.clear_charts)
|
log.warning(
|
||||||
|
"https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=repo"
|
||||||
self.get_symbol_list()
|
)
|
||||||
schedule.every().day.do(self.get_symbol_list)
|
|
||||||
|
if self.MARKETDATA_TOKEN != "":
|
||||||
def get(self, endpoint, params=None, timeout=10, headers=None) -> dict:
|
schedule.every().day.do(self.clear_charts)
|
||||||
url = "https://api.marketdata.app/v1/" + endpoint
|
|
||||||
|
self.get_symbol_list()
|
||||||
if params is None:
|
schedule.every().day.do(self.get_symbol_list)
|
||||||
params = {}
|
|
||||||
|
def get(self, endpoint, params=None, timeout=10, headers=None) -> dict:
|
||||||
# set token param if it wasn't passed.
|
url = "https://api.marketdata.app/v1/" + endpoint
|
||||||
params["token"] = self.MARKETDATA_TOKEN
|
|
||||||
|
if params is None:
|
||||||
# Undocumented query variable that ensures bot usage can be
|
params = {}
|
||||||
# monitored even if someone doesn't make it through an affiliate link.
|
|
||||||
params["application"] = "simplestockbot"
|
# set token param if it wasn't passed.
|
||||||
|
params["token"] = self.MARKETDATA_TOKEN
|
||||||
if headers is None:
|
|
||||||
headers = {}
|
# Undocumented query variable that ensures bot usage can be
|
||||||
headers = {"User-Agent": "Simple Stock Bot anson@ansonbiggs.com"} | headers
|
# monitored even if someone doesn't make it through an affiliate link.
|
||||||
|
params["application"] = "simplestockbot"
|
||||||
resp = r.get(url, params=params, timeout=timeout, headers=headers)
|
|
||||||
|
if headers is None:
|
||||||
logging.error(resp.headers.items())
|
headers = {}
|
||||||
|
headers = {"User-Agent": "Simple Stock Bot anson@ansonbiggs.com"} | headers
|
||||||
# Make sure API returned a proper status code
|
|
||||||
try:
|
resp = r.get(url, params=params, timeout=timeout, headers=headers)
|
||||||
resp.raise_for_status()
|
|
||||||
except r.exceptions.HTTPError as e:
|
logging.error(resp.headers.items())
|
||||||
logging.error(e)
|
|
||||||
return {}
|
# Make sure API returned a proper status code
|
||||||
|
try:
|
||||||
# Make sure API returned valid JSON
|
resp.raise_for_status()
|
||||||
try:
|
except r.exceptions.HTTPError as e:
|
||||||
resp_json = resp.json()
|
logging.error(e)
|
||||||
|
return {}
|
||||||
match resp_json["s"]:
|
|
||||||
case "ok":
|
# Make sure API returned valid JSON
|
||||||
return resp_json
|
try:
|
||||||
case "no_data":
|
resp_json = resp.json()
|
||||||
return resp_json
|
|
||||||
case "error":
|
match resp_json["s"]:
|
||||||
logging.error("MarketData Error:\n" + resp_json["errmsg"])
|
case "ok":
|
||||||
return {}
|
return resp_json
|
||||||
|
case "no_data":
|
||||||
except r.exceptions.JSONDecodeError as e:
|
return resp_json
|
||||||
logging.error(e)
|
case "error":
|
||||||
|
logging.error("MarketData Error:\n" + resp_json["errmsg"])
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def symbol_id(self, symbol: str) -> Dict[str, Dict]:
|
except r.exceptions.JSONDecodeError as e:
|
||||||
return self.symbol_list.get(symbol.upper(), None)
|
logging.error(e)
|
||||||
|
|
||||||
def get_symbol_list(self):
|
return {}
|
||||||
# Doesn't use `self.get`` since needs are much different
|
|
||||||
sec_resp = r.get(
|
def symbol_id(self, symbol: str) -> Dict[str, Dict]:
|
||||||
"https://www.sec.gov/files/company_tickers.json",
|
return self.symbol_list.get(symbol.upper(), None)
|
||||||
headers={
|
|
||||||
"User-Agent": "Simple Stock Bot anson@ansonbiggs.com",
|
def get_symbol_list(self):
|
||||||
"Accept-Encoding": "gzip, deflate",
|
# Doesn't use `self.get()` since needs are much different
|
||||||
"Host": "www.sec.gov",
|
sec_resp = r.get(
|
||||||
},
|
"https://www.sec.gov/files/company_tickers.json",
|
||||||
)
|
headers={
|
||||||
sec_resp.raise_for_status()
|
"User-Agent": "Simple Stock Bot anson@ansonbiggs.com",
|
||||||
sec_data = sec_resp.json()
|
"Accept-Encoding": "gzip, deflate",
|
||||||
|
"Host": "www.sec.gov",
|
||||||
for rank, ticker_info in sec_data.items():
|
},
|
||||||
self.symbol_list[ticker_info["ticker"]] = {
|
)
|
||||||
"ticker": ticker_info["ticker"],
|
sec_resp.raise_for_status()
|
||||||
"title": ticker_info["title"],
|
sec_data = sec_resp.json()
|
||||||
"mkt_cap_rank": rank,
|
|
||||||
}
|
for rank, ticker_info in sec_data.items():
|
||||||
|
self.symbol_list[ticker_info["ticker"]] = {
|
||||||
def clear_charts(self) -> None:
|
"ticker": ticker_info["ticker"],
|
||||||
"""
|
"title": ticker_info["title"],
|
||||||
Clears cache of chart data.
|
"mkt_cap_rank": rank,
|
||||||
Charts are cached so that only 1 API call per 24 hours is needed since the
|
}
|
||||||
chart data is expensive and a large download.
|
|
||||||
"""
|
def clear_charts(self) -> None:
|
||||||
self.charts = {}
|
"""
|
||||||
|
Clears cache of chart data.
|
||||||
def status(self) -> str:
|
Charts are cached so that only 1 API call per 24 hours is needed since the
|
||||||
# TODO: At the moment this API is poorly documented, this function likely needs to be revisited later.
|
chart data is expensive and a large download.
|
||||||
|
"""
|
||||||
try:
|
self.charts = {}
|
||||||
status = r.get(
|
|
||||||
"https://stats.uptimerobot.com/api/getMonitorList/6Kv3zIow0A",
|
def status(self) -> str:
|
||||||
timeout=5,
|
# TODO: At the moment this API is poorly documented, this function likely needs to be revisited later.
|
||||||
)
|
|
||||||
status.raise_for_status()
|
try:
|
||||||
except r.HTTPError:
|
status = r.get(
|
||||||
return f"API returned an HTTP error code {status.status_code} in {status.elapsed.total_seconds()} seconds."
|
"https://stats.uptimerobot.com/api/getMonitorList/6Kv3zIow0A",
|
||||||
except r.Timeout:
|
timeout=5,
|
||||||
return "API timed out before it was able to give status. This is likely due to a surge in usage or a complete outage."
|
)
|
||||||
|
status.raise_for_status()
|
||||||
statusJSON = status.json()
|
except r.HTTPError:
|
||||||
|
return f"API returned an HTTP error code {status.status_code} in {status.elapsed.total_seconds()} seconds."
|
||||||
if statusJSON["status"] == "ok":
|
except r.Timeout:
|
||||||
return (
|
return "API timed out before it was able to give status. This is likely due to a surge in usage or a complete outage."
|
||||||
f"CoinGecko API responded that it was OK with a {status.status_code} in {status.elapsed.total_seconds()} seconds."
|
|
||||||
)
|
statusJSON = status.json()
|
||||||
else:
|
|
||||||
return f"MarketData.app is currently reporting the following status: {statusJSON['status']}"
|
if statusJSON["status"] == "ok":
|
||||||
|
return f"CoinGecko API responded that it was OK with a {status.status_code} in {status.elapsed.total_seconds()} seconds."
|
||||||
def price_reply(self, symbol: Stock) -> str:
|
else:
|
||||||
"""Returns price movement of Stock for the last market day, or after hours.
|
return f"MarketData.app is currently reporting the following status: {statusJSON['status']}"
|
||||||
|
|
||||||
Parameters
|
def price_reply(self, symbol: Stock) -> str:
|
||||||
----------
|
"""Returns price movement of Stock for the last market day, or after hours.
|
||||||
symbol : Stock
|
|
||||||
|
Parameters
|
||||||
Returns
|
----------
|
||||||
-------
|
symbol : Stock
|
||||||
str
|
|
||||||
Formatted markdown
|
Returns
|
||||||
"""
|
-------
|
||||||
|
str
|
||||||
if quoteResp := self.get(f"stocks/quotes/{symbol.symbol}/"):
|
Formatted markdown
|
||||||
price = round(quoteResp["last"][0], 2)
|
"""
|
||||||
|
|
||||||
try:
|
if quoteResp := self.get(f"stocks/quotes/{symbol.symbol}/"):
|
||||||
changePercent = round(quoteResp["changepct"][0], 2)
|
price = round(quoteResp["last"][0], 2)
|
||||||
except TypeError:
|
|
||||||
return f"The price of {symbol.name} is ${price}"
|
try:
|
||||||
|
changePercent = round(quoteResp["changepct"][0], 2)
|
||||||
message = f"The current price of {symbol.name} is ${price} and "
|
except TypeError:
|
||||||
|
return f"The price of {symbol.name} is ${price}"
|
||||||
if changePercent > 0.0:
|
|
||||||
message += f"is currently up {changePercent}% for the day."
|
message = f"The current price of {symbol.name} is ${price} and "
|
||||||
elif changePercent < 0.0:
|
|
||||||
message += f"is currently down {changePercent}% for the day."
|
if changePercent > 0.0:
|
||||||
else:
|
message += f"is currently up {changePercent}% for the day."
|
||||||
message += "hasn't shown any movement for the day."
|
elif changePercent < 0.0:
|
||||||
|
message += f"is currently down {changePercent}% for the day."
|
||||||
return message
|
else:
|
||||||
else:
|
message += "hasn't shown any movement for the day."
|
||||||
return f"Getting a quote for {symbol} encountered an error."
|
|
||||||
|
return message
|
||||||
def spark_reply(self, symbol: Stock) -> str:
|
else:
|
||||||
if quoteResp := self.get(f"stocks/quotes/{symbol}/"):
|
return f"Getting a quote for {symbol} encountered an error."
|
||||||
try:
|
|
||||||
changePercent = round(quoteResp["changepct"][0], 2)
|
def spark_reply(self, symbol: Stock) -> str:
|
||||||
return f"`{symbol.tag}`: {changePercent}%"
|
if quoteResp := self.get(f"stocks/quotes/{symbol}/"):
|
||||||
except TypeError:
|
try:
|
||||||
pass
|
changePercent = round(quoteResp["changepct"][0], 2)
|
||||||
|
return f"`{symbol.tag}`: {changePercent}%"
|
||||||
return f"`{symbol.tag}`"
|
except TypeError:
|
||||||
|
pass
|
||||||
def intra_reply(self, symbol: Stock) -> pd.DataFrame:
|
|
||||||
"""Returns price data for a symbol of the past month up until the previous trading days close.
|
return f"`{symbol.tag}`"
|
||||||
Also caches multiple requests made in the same day.
|
|
||||||
|
def intra_reply(self, symbol: Stock) -> pd.DataFrame:
|
||||||
Parameters
|
"""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.
|
||||||
symbol : str
|
|
||||||
Stock symbol.
|
Parameters
|
||||||
|
----------
|
||||||
Returns
|
symbol : str
|
||||||
-------
|
Stock symbol.
|
||||||
pd.DataFrame
|
|
||||||
Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
|
Returns
|
||||||
"""
|
-------
|
||||||
schedule.run_pending()
|
pd.DataFrame
|
||||||
|
Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
|
||||||
try:
|
"""
|
||||||
return self.charts[symbol.id.upper()]
|
schedule.run_pending()
|
||||||
except KeyError:
|
|
||||||
pass
|
try:
|
||||||
|
return self.charts[symbol.id.upper()]
|
||||||
resolution = "15" # minutes
|
except KeyError:
|
||||||
now = dt.datetime.now(self.marketTimeZone)
|
pass
|
||||||
|
|
||||||
if self.openTime < now.time():
|
resolution = "15" # minutes
|
||||||
startTime = now.replace(hour=9, minute=30)
|
now = dt.datetime.now(self.marketTimeZone)
|
||||||
else:
|
|
||||||
startTime = now - dt.timedelta(days=1)
|
if self.openTime < now.time():
|
||||||
|
startTime = now.replace(hour=9, minute=30)
|
||||||
if data := self.get(
|
else:
|
||||||
f"stocks/candles/{resolution}/{symbol}",
|
startTime = now - dt.timedelta(days=1)
|
||||||
params={"from": startTime.timestamp(), "to": now.timestamp(), "extended": True},
|
|
||||||
):
|
if data := self.get(
|
||||||
data.pop("s")
|
f"stocks/candles/{resolution}/{symbol}",
|
||||||
df = pd.DataFrame(data)
|
params={
|
||||||
df["t"] = pd.to_datetime(df["t"], unit="s", utc=True)
|
"from": startTime.timestamp(),
|
||||||
df.set_index("t", inplace=True)
|
"to": now.timestamp(),
|
||||||
|
"extended": True,
|
||||||
df.rename(
|
},
|
||||||
columns={
|
):
|
||||||
"o": "Open",
|
data.pop("s")
|
||||||
"h": "High",
|
df = pd.DataFrame(data)
|
||||||
"l": "Low",
|
df["t"] = pd.to_datetime(df["t"], unit="s", utc=True)
|
||||||
"c": "Close",
|
df.set_index("t", inplace=True)
|
||||||
"v": "Volume",
|
|
||||||
},
|
df.rename(
|
||||||
inplace=True,
|
columns={
|
||||||
)
|
"o": "Open",
|
||||||
|
"h": "High",
|
||||||
self.charts[symbol.id.upper()] = df
|
"l": "Low",
|
||||||
return df
|
"c": "Close",
|
||||||
|
"v": "Volume",
|
||||||
return pd.DataFrame()
|
},
|
||||||
|
inplace=True,
|
||||||
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.
|
self.charts[symbol.id.upper()] = df
|
||||||
|
return df
|
||||||
Parameters
|
|
||||||
----------
|
return pd.DataFrame()
|
||||||
symbol : str
|
|
||||||
Stock symbol.
|
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.
|
||||||
Returns
|
Also caches multiple requests made in the same day.
|
||||||
-------
|
|
||||||
pd.DataFrame
|
Parameters
|
||||||
Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
|
----------
|
||||||
"""
|
symbol : str
|
||||||
schedule.run_pending()
|
Stock symbol.
|
||||||
|
|
||||||
try:
|
Returns
|
||||||
return self.charts[symbol.id.upper()]
|
-------
|
||||||
except KeyError:
|
pd.DataFrame
|
||||||
pass
|
Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
|
||||||
|
"""
|
||||||
to_date = dt.datetime.today().strftime("%Y-%m-%d")
|
schedule.run_pending()
|
||||||
from_date = (dt.datetime.today() - dt.timedelta(days=30)).strftime("%Y-%m-%d")
|
|
||||||
resultion = "daily"
|
try:
|
||||||
|
return self.charts[symbol.id.upper()]
|
||||||
if data := self.get(
|
except KeyError:
|
||||||
f"stocks/candles/{resultion}/{symbol}",
|
pass
|
||||||
params={
|
|
||||||
"from": from_date,
|
to_date = dt.datetime.today().strftime("%Y-%m-%d")
|
||||||
"to": to_date,
|
from_date = (dt.datetime.today() - dt.timedelta(days=30)).strftime("%Y-%m-%d")
|
||||||
},
|
resultion = "daily"
|
||||||
):
|
|
||||||
data.pop("s")
|
if data := self.get(
|
||||||
|
f"stocks/candles/{resultion}/{symbol}",
|
||||||
df = pd.DataFrame(data)
|
params={
|
||||||
df["t"] = pd.to_datetime(df["t"], unit="s")
|
"from": from_date,
|
||||||
df.set_index("t", inplace=True)
|
"to": to_date,
|
||||||
|
},
|
||||||
df.rename(
|
):
|
||||||
columns={
|
data.pop("s")
|
||||||
"o": "Open",
|
|
||||||
"h": "High",
|
df = pd.DataFrame(data)
|
||||||
"l": "Low",
|
df["t"] = pd.to_datetime(df["t"], unit="s")
|
||||||
"c": "Close",
|
df.set_index("t", inplace=True)
|
||||||
"v": "Volume",
|
|
||||||
},
|
df.rename(
|
||||||
inplace=True,
|
columns={
|
||||||
)
|
"o": "Open",
|
||||||
|
"h": "High",
|
||||||
self.charts[symbol.id.upper()] = df
|
"l": "Low",
|
||||||
return df
|
"c": "Close",
|
||||||
|
"v": "Volume",
|
||||||
return pd.DataFrame()
|
},
|
||||||
|
inplace=True,
|
||||||
def options_reply(self, request: str) -> str:
|
)
|
||||||
"""Undocumented API Usage!"""
|
|
||||||
|
self.charts[symbol.id.upper()] = df
|
||||||
options_data = self.get(f"options/quotes/{request}")
|
return df
|
||||||
|
|
||||||
for key in options_data.keys():
|
return pd.DataFrame()
|
||||||
options_data[key] = options_data[key][0]
|
|
||||||
|
def options_reply(self, request: str) -> str:
|
||||||
options_data["underlying"] = "$" + options_data["underlying"]
|
"""Undocumented API Usage!"""
|
||||||
|
|
||||||
options_data["updated"] = humanize.naturaltime(dt.datetime.now() - dt.datetime.fromtimestamp(options_data["updated"]))
|
options_data = self.get(f"options/quotes/{request}")
|
||||||
|
|
||||||
options_data["expiration"] = humanize.naturaltime(
|
for key in options_data.keys():
|
||||||
dt.datetime.now() - dt.datetime.fromtimestamp(options_data["expiration"])
|
options_data[key] = options_data[key][0]
|
||||||
)
|
|
||||||
|
options_data["underlying"] = "$" + options_data["underlying"]
|
||||||
options_data["firstTraded"] = humanize.naturaltime(
|
|
||||||
dt.datetime.now() - dt.datetime.fromtimestamp(options_data["firstTraded"])
|
options_data["updated"] = humanize.naturaltime(
|
||||||
)
|
dt.datetime.now() - dt.datetime.fromtimestamp(options_data["updated"])
|
||||||
|
)
|
||||||
rename = {
|
|
||||||
"optionSymbol": "Option Symbol",
|
options_data["expiration"] = humanize.naturaltime(
|
||||||
"underlying": "Underlying",
|
dt.datetime.now() - dt.datetime.fromtimestamp(options_data["expiration"])
|
||||||
"expiration": "Expiration",
|
)
|
||||||
"side": "side",
|
|
||||||
"strike": "strike",
|
options_data["firstTraded"] = humanize.naturaltime(
|
||||||
"firstTraded": "First Traded",
|
dt.datetime.now() - dt.datetime.fromtimestamp(options_data["firstTraded"])
|
||||||
"updated": "Last Updated",
|
)
|
||||||
"bid": "bid",
|
|
||||||
"bidSize": "bidSize",
|
rename = {
|
||||||
"mid": "mid",
|
"optionSymbol": "Option Symbol",
|
||||||
"ask": "ask",
|
"underlying": "Underlying",
|
||||||
"askSize": "askSize",
|
"expiration": "Expiration",
|
||||||
"last": "last",
|
"side": "side",
|
||||||
"openInterest": "Open Interest",
|
"strike": "strike",
|
||||||
"volume": "Volume",
|
"firstTraded": "First Traded",
|
||||||
"inTheMoney": "inTheMoney",
|
"updated": "Last Updated",
|
||||||
"intrinsicValue": "Intrinsic Value",
|
"bid": "bid",
|
||||||
"extrinsicValue": "Extrinsic Value",
|
"bidSize": "bidSize",
|
||||||
"underlyingPrice": "Underlying Price",
|
"mid": "mid",
|
||||||
"iv": "Implied Volatility",
|
"ask": "ask",
|
||||||
"delta": "delta",
|
"askSize": "askSize",
|
||||||
"gamma": "gamma",
|
"last": "last",
|
||||||
"theta": "theta",
|
"openInterest": "Open Interest",
|
||||||
"vega": "vega",
|
"volume": "Volume",
|
||||||
"rho": "rho",
|
"inTheMoney": "inTheMoney",
|
||||||
}
|
"intrinsicValue": "Intrinsic Value",
|
||||||
|
"extrinsicValue": "Extrinsic Value",
|
||||||
options_cleaned = OrderedDict()
|
"underlyingPrice": "Underlying Price",
|
||||||
for old, new in rename.items():
|
"iv": "Implied Volatility",
|
||||||
if old in options_data:
|
"delta": "delta",
|
||||||
options_cleaned[new] = options_data[old]
|
"gamma": "gamma",
|
||||||
|
"theta": "theta",
|
||||||
return options_cleaned
|
"vega": "vega",
|
||||||
|
"rho": "rho",
|
||||||
|
}
|
||||||
|
|
||||||
|
options_cleaned = OrderedDict()
|
||||||
|
for old, new in rename.items():
|
||||||
|
if old in options_data:
|
||||||
|
options_cleaned[new] = options_data[old]
|
||||||
|
|
||||||
|
return options_cleaned
|
||||||
|
107
common/Symbol.py
107
common/Symbol.py
@ -1,52 +1,55 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
class Symbol:
|
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
|
tag: Uppercase tag to call the symbol. ie $TSLA or $$BTC
|
||||||
"""
|
"""
|
||||||
|
|
||||||
currency = "usd"
|
currency = "usd"
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __init__(self, symbol) -> None:
|
def __init__(self, symbol) -> None:
|
||||||
self.symbol = symbol
|
self.symbol = symbol
|
||||||
self.id = symbol
|
self.id = symbol
|
||||||
self.name = symbol
|
self.name = symbol
|
||||||
self.tag = "$" + 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)}>"
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.id
|
return self.id
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
class Stock(Symbol):
|
return hash(self.id)
|
||||||
"""Stock Market Object. Gets data from MarketData"""
|
|
||||||
|
|
||||||
def __init__(self, symbol_info: dict) -> None:
|
class Stock(Symbol):
|
||||||
self.symbol = symbol_info["ticker"]
|
"""Stock Market Object. Gets data from MarketData"""
|
||||||
self.id = symbol_info["ticker"]
|
|
||||||
self.name = symbol_info["title"]
|
def __init__(self, symbol_info: dict) -> None:
|
||||||
self.tag = "$" + symbol_info["ticker"]
|
self.symbol = symbol_info["ticker"]
|
||||||
self.market_cap_rank = symbol_info["mkt_cap_rank"]
|
self.id = symbol_info["ticker"]
|
||||||
|
self.name = symbol_info["title"]
|
||||||
|
self.tag = "$" + symbol_info["ticker"]
|
||||||
class Coin(Symbol):
|
self.market_cap_rank = symbol_info["mkt_cap_rank"]
|
||||||
"""Cryptocurrency Object. Gets data from CoinGecko."""
|
|
||||||
|
|
||||||
def __init__(self, symbol: pd.DataFrame) -> None:
|
class Coin(Symbol):
|
||||||
if len(symbol) > 1:
|
"""Cryptocurrency Object. Gets data from CoinGecko."""
|
||||||
logging.info(f"Crypto with shared id:\n\t{symbol.id}")
|
|
||||||
symbol = symbol.head(1)
|
def __init__(self, symbol: pd.DataFrame) -> None:
|
||||||
|
if len(symbol) > 1:
|
||||||
self.symbol = symbol.symbol.values[0]
|
logging.info(f"Crypto with shared id:\n\t{symbol.id}")
|
||||||
self.id = symbol.id.values[0]
|
symbol = symbol.head(1)
|
||||||
self.name = symbol.name.values[0]
|
|
||||||
self.tag = symbol.type_id.values[0].upper()
|
self.symbol = symbol.symbol.values[0]
|
||||||
|
self.id = symbol.id.values[0]
|
||||||
|
self.name = symbol.name.values[0]
|
||||||
|
self.tag = symbol.type_id.values[0].upper()
|
||||||
|
@ -1,381 +1,388 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import requests as r
|
import requests as r
|
||||||
import schedule
|
import schedule
|
||||||
from markdownify import markdownify
|
from markdownify import markdownify
|
||||||
|
from common.Symbol import Coin
|
||||||
from common.Symbol import Coin
|
from common.utilities import rate_limited
|
||||||
from common.utilities import rate_limited
|
|
||||||
|
import time
|
||||||
import time
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
class cg_Crypto:
|
||||||
class cg_Crypto:
|
"""
|
||||||
"""
|
Functions for finding crypto info
|
||||||
Functions for finding crypto info
|
"""
|
||||||
"""
|
|
||||||
|
vs_currency = "usd" # simple/supported_vs_currencies for list of options
|
||||||
vs_currency = "usd" # simple/supported_vs_currencies for list of options
|
|
||||||
|
trending_cache: List[str] = []
|
||||||
trending_cache: List[str] = []
|
|
||||||
|
def __init__(self) -> None:
|
||||||
def __init__(self) -> None:
|
self.get_symbol_list()
|
||||||
self.get_symbol_list()
|
schedule.every().day.do(self.get_symbol_list)
|
||||||
schedule.every().day.do(self.get_symbol_list)
|
|
||||||
|
# Coingecko's rate limit is 30 requests per minute.
|
||||||
# Coingecko's rate limit is 30 requests per minute.
|
# Since there are two bots sharing the same IP, we allocate half of that limit to each bot.
|
||||||
# Since there are two bots sharing the same IP, we allocate half of that limit to each bot.
|
# This results in a rate limit of 15 requests per minute for each bot.
|
||||||
# This results in a rate limit of 15 requests per minute for each bot.
|
# Given this, the rate limit effectively becomes 1 request every 4 seconds for each bot.
|
||||||
# Given this, the rate limit effectively becomes 1 request every 4 seconds for each bot.
|
@rate_limited(0.25)
|
||||||
@rate_limited(0.25)
|
def get(self, endpoint, params: dict = {}, timeout=10) -> dict:
|
||||||
def get(self, endpoint, params: dict = {}, timeout=10) -> dict:
|
url = "https://api.coingecko.com/api/v3" + endpoint
|
||||||
url = "https://api.coingecko.com/api/v3" + endpoint
|
resp = r.get(url, params=params, timeout=timeout)
|
||||||
resp = r.get(url, params=params, timeout=timeout)
|
# Make sure API returned a proper status code
|
||||||
# Make sure API returned a proper status code
|
|
||||||
|
if resp.status_code == 429:
|
||||||
if resp.status_code == 429:
|
log.warning(
|
||||||
log.warning(f"CoinGecko returned 429 - Too Many Requests for endpoint: {endpoint}. Sleeping and trying again.")
|
f"CoinGecko returned 429 - Too Many Requests for endpoint: {endpoint}. Sleeping and trying again."
|
||||||
time.sleep(10)
|
)
|
||||||
return self.get(endpoint=endpoint, params=params, timeout=timeout)
|
time.sleep(10)
|
||||||
|
return self.get(endpoint=endpoint, params=params, timeout=timeout)
|
||||||
try:
|
|
||||||
resp.raise_for_status()
|
try:
|
||||||
except r.exceptions.HTTPError as e:
|
resp.raise_for_status()
|
||||||
log.error(e)
|
except r.exceptions.HTTPError as e:
|
||||||
return {}
|
log.error(e)
|
||||||
|
return {}
|
||||||
# Make sure API returned valid JSON
|
|
||||||
try:
|
# Make sure API returned valid JSON
|
||||||
resp_json = resp.json()
|
try:
|
||||||
return resp_json
|
resp_json = resp.json()
|
||||||
except r.exceptions.JSONDecodeError as e:
|
return resp_json
|
||||||
log.error(e)
|
except r.exceptions.JSONDecodeError as e:
|
||||||
return {}
|
log.error(e)
|
||||||
|
return {}
|
||||||
def symbol_id(self, symbol) -> str:
|
|
||||||
try:
|
def symbol_id(self, symbol) -> str:
|
||||||
return self.symbol_list[self.symbol_list["symbol"] == symbol]["id"].values[0]
|
try:
|
||||||
except KeyError:
|
return self.symbol_list[self.symbol_list["symbol"] == symbol]["id"].values[
|
||||||
return ""
|
0
|
||||||
|
]
|
||||||
def get_symbol_list(self):
|
except KeyError:
|
||||||
raw_symbols = self.get("/coins/list")
|
return ""
|
||||||
symbols = pd.DataFrame(data=raw_symbols)
|
|
||||||
|
def get_symbol_list(self):
|
||||||
# Removes all binance-peg symbols
|
raw_symbols = self.get("/coins/list")
|
||||||
symbols = symbols[~symbols["id"].str.contains("binance-peg")]
|
symbols = pd.DataFrame(data=raw_symbols)
|
||||||
|
|
||||||
symbols["description"] = "$$" + symbols["symbol"].str.upper() + ": " + symbols["name"]
|
# Removes all binance-peg symbols
|
||||||
symbols = symbols[["id", "symbol", "name", "description"]]
|
symbols = symbols[~symbols["id"].str.contains("binance-peg")]
|
||||||
symbols["type_id"] = "$$" + symbols["symbol"]
|
|
||||||
|
symbols["description"] = (
|
||||||
self.symbol_list = symbols
|
"$$" + symbols["symbol"].str.upper() + ": " + symbols["name"]
|
||||||
|
)
|
||||||
def status(self) -> str:
|
symbols = symbols[["id", "symbol", "name", "description"]]
|
||||||
"""Checks CoinGecko /ping endpoint for API issues.
|
symbols["type_id"] = "$$" + symbols["symbol"]
|
||||||
|
|
||||||
Returns
|
self.symbol_list = symbols
|
||||||
-------
|
|
||||||
str
|
def status(self) -> str:
|
||||||
Human readable text on status of CoinGecko API
|
"""Checks CoinGecko /ping endpoint for API issues.
|
||||||
"""
|
|
||||||
status = r.get(
|
Returns
|
||||||
"https://api.coingecko.com/api/v3/ping",
|
-------
|
||||||
timeout=5,
|
str
|
||||||
)
|
Human readable text on status of CoinGecko API
|
||||||
|
"""
|
||||||
try:
|
status = r.get(
|
||||||
status.raise_for_status()
|
"https://api.coingecko.com/api/v3/ping",
|
||||||
return (
|
timeout=5,
|
||||||
f"CoinGecko API responded that it was OK with a {status.status_code} in {status.elapsed.total_seconds()} seconds."
|
)
|
||||||
)
|
|
||||||
except r.HTTPError:
|
try:
|
||||||
return f"CoinGecko API returned an error code {status.status_code} in {status.elapsed.total_seconds()} seconds."
|
status.raise_for_status()
|
||||||
|
return f"CoinGecko API responded that it was OK with a {status.status_code} in {status.elapsed.total_seconds()} seconds."
|
||||||
def price_reply(self, coin: Coin) -> str:
|
except r.HTTPError:
|
||||||
"""Returns current market price or after hours if its available for a given coin symbol.
|
return f"CoinGecko API returned an error code {status.status_code} in {status.elapsed.total_seconds()} seconds."
|
||||||
|
|
||||||
Parameters
|
def price_reply(self, coin: Coin) -> str:
|
||||||
----------
|
"""Returns current market price or after hours if its available for a given coin symbol.
|
||||||
symbols : list
|
|
||||||
List of coin symbols.
|
Parameters
|
||||||
|
----------
|
||||||
Returns
|
symbols : list
|
||||||
-------
|
List of coin symbols.
|
||||||
Dict[str, str]
|
|
||||||
Each symbol passed in is a key with its value being a human readable
|
Returns
|
||||||
markdown formatted string of the symbols price and movement.
|
-------
|
||||||
"""
|
Dict[str, str]
|
||||||
|
Each symbol passed in is a key with its value being a human readable
|
||||||
if resp := self.get(
|
markdown formatted string of the symbols price and movement.
|
||||||
"/simple/price",
|
"""
|
||||||
params={
|
|
||||||
"ids": coin.id,
|
if resp := self.get(
|
||||||
"vs_currencies": self.vs_currency,
|
"/simple/price",
|
||||||
"include_24hr_change": "true",
|
params={
|
||||||
},
|
"ids": coin.id,
|
||||||
):
|
"vs_currencies": self.vs_currency,
|
||||||
try:
|
"include_24hr_change": "true",
|
||||||
data = resp[coin.id]
|
},
|
||||||
|
):
|
||||||
price = data[self.vs_currency]
|
try:
|
||||||
change = data[self.vs_currency + "_24h_change"]
|
data = resp[coin.id]
|
||||||
if change is None:
|
|
||||||
change = 0
|
price = data[self.vs_currency]
|
||||||
except KeyError:
|
change = data[self.vs_currency + "_24h_change"]
|
||||||
return f"{coin.id} returned an error."
|
if change is None:
|
||||||
|
change = 0
|
||||||
message = f"The current price of {coin.name} is $**{price:,}**"
|
except KeyError:
|
||||||
|
return f"{coin.id} returned an error."
|
||||||
# Determine wording of change text
|
|
||||||
if change > 0:
|
message = f"The current price of {coin.name} is $**{price:,}**"
|
||||||
message += f", the coin is currently **up {change:.3f}%** for today"
|
|
||||||
elif change < 0:
|
# Determine wording of change text
|
||||||
message += f", the coin is currently **down {change:.3f}%** for today"
|
if change > 0:
|
||||||
else:
|
message += f", the coin is currently **up {change:.3f}%** for today"
|
||||||
message += ", the coin hasn't shown any movement today."
|
elif change < 0:
|
||||||
|
message += f", the coin is currently **down {change:.3f}%** for today"
|
||||||
else:
|
else:
|
||||||
message = f"The price for {coin.name} is not available. If you suspect this is an error run `/status`"
|
message += ", the coin hasn't shown any movement today."
|
||||||
|
|
||||||
return message
|
else:
|
||||||
|
message = f"The price for {coin.name} is not available. If you suspect this is an error run `/status`"
|
||||||
def intra_reply(self, symbol: Coin) -> pd.DataFrame:
|
|
||||||
"""Returns price data for a symbol since the last market open.
|
return message
|
||||||
|
|
||||||
Parameters
|
def intra_reply(self, symbol: Coin) -> pd.DataFrame:
|
||||||
----------
|
"""Returns price data for a symbol since the last market open.
|
||||||
symbol : str
|
|
||||||
Stock symbol.
|
Parameters
|
||||||
|
----------
|
||||||
Returns
|
symbol : str
|
||||||
-------
|
Stock symbol.
|
||||||
pd.DataFrame
|
|
||||||
Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
|
Returns
|
||||||
"""
|
-------
|
||||||
|
pd.DataFrame
|
||||||
if resp := self.get(
|
Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
|
||||||
f"/coins/{symbol.id}/ohlc",
|
"""
|
||||||
params={"vs_currency": self.vs_currency, "days": 1},
|
|
||||||
):
|
if resp := self.get(
|
||||||
df = pd.DataFrame(resp, columns=["Date", "Open", "High", "Low", "Close"]).dropna()
|
f"/coins/{symbol.id}/ohlc",
|
||||||
df["Date"] = pd.to_datetime(df["Date"], unit="ms")
|
params={"vs_currency": self.vs_currency, "days": 1},
|
||||||
df = df.set_index("Date")
|
):
|
||||||
return df
|
df = pd.DataFrame(
|
||||||
|
resp, columns=["Date", "Open", "High", "Low", "Close"]
|
||||||
return pd.DataFrame()
|
).dropna()
|
||||||
|
df["Date"] = pd.to_datetime(df["Date"], unit="ms")
|
||||||
def chart_reply(self, symbol: Coin) -> pd.DataFrame:
|
df = df.set_index("Date")
|
||||||
"""Returns price data for a symbol of the past month up until the previous trading days close.
|
return df
|
||||||
Also caches multiple requests made in the same day.
|
|
||||||
|
return pd.DataFrame()
|
||||||
Parameters
|
|
||||||
----------
|
def chart_reply(self, symbol: Coin) -> pd.DataFrame:
|
||||||
symbol : str
|
"""Returns price data for a symbol of the past month up until the previous trading days close.
|
||||||
Stock symbol.
|
Also caches multiple requests made in the same day.
|
||||||
|
|
||||||
Returns
|
Parameters
|
||||||
-------
|
----------
|
||||||
pd.DataFrame
|
symbol : str
|
||||||
Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
|
Stock symbol.
|
||||||
"""
|
|
||||||
|
Returns
|
||||||
if resp := self.get(
|
-------
|
||||||
f"/coins/{symbol.id}/ohlc",
|
pd.DataFrame
|
||||||
params={"vs_currency": self.vs_currency, "days": 30},
|
Returns a timeseries dataframe with high, low, and volume data if its available. Otherwise returns empty pd.DataFrame.
|
||||||
):
|
"""
|
||||||
df = pd.DataFrame(resp, columns=["Date", "Open", "High", "Low", "Close"]).dropna()
|
|
||||||
df["Date"] = pd.to_datetime(df["Date"], unit="ms")
|
if resp := self.get(
|
||||||
df = df.set_index("Date")
|
f"/coins/{symbol.id}/ohlc",
|
||||||
return df
|
params={"vs_currency": self.vs_currency, "days": 30},
|
||||||
|
):
|
||||||
return pd.DataFrame()
|
df = pd.DataFrame(
|
||||||
|
resp, columns=["Date", "Open", "High", "Low", "Close"]
|
||||||
def stat_reply(self, symbol: Coin) -> str:
|
).dropna()
|
||||||
"""Gathers key statistics on coin. Mostly just CoinGecko scores.
|
df["Date"] = pd.to_datetime(df["Date"], unit="ms")
|
||||||
|
df = df.set_index("Date")
|
||||||
Parameters
|
return df
|
||||||
----------
|
|
||||||
symbol : Coin
|
return pd.DataFrame()
|
||||||
|
|
||||||
Returns
|
def stat_reply(self, symbol: Coin) -> str:
|
||||||
-------
|
"""Gathers key statistics on coin. Mostly just CoinGecko scores.
|
||||||
str
|
|
||||||
Preformatted markdown.
|
Parameters
|
||||||
"""
|
----------
|
||||||
|
symbol : Coin
|
||||||
if data := self.get(
|
|
||||||
f"/coins/{symbol.id}",
|
Returns
|
||||||
params={
|
-------
|
||||||
"localization": "false",
|
str
|
||||||
},
|
Preformatted markdown.
|
||||||
):
|
"""
|
||||||
return f"""
|
|
||||||
[{data['name']}]({data['links']['homepage'][0]}) Statistics:
|
if data := self.get(
|
||||||
Market Cap: ${data['market_data']['market_cap'][self.vs_currency]:,}
|
f"/coins/{symbol.id}",
|
||||||
Market Cap Ranking: {data.get('market_cap_rank',"Not Available")}
|
params={
|
||||||
CoinGecko Scores:
|
"localization": "false",
|
||||||
Overall: {data.get('coingecko_score','Not Available')}
|
},
|
||||||
Development: {data.get('developer_score','Not Available')}
|
):
|
||||||
Community: {data.get('community_score','Not Available')}
|
return f"""
|
||||||
Public Interest: {data.get('public_interest_score','Not Available')}
|
[{data['name']}]({data['links']['homepage'][0]}) Statistics:
|
||||||
"""
|
Market Cap: ${data['market_data']['market_cap'][self.vs_currency]:,}
|
||||||
else:
|
Market Cap Ranking: {data.get('market_cap_rank',"Not Available")}
|
||||||
return f"{symbol.symbol} returned an error."
|
CoinGecko Scores:
|
||||||
|
Overall: {data.get('coingecko_score','Not Available')}
|
||||||
def cap_reply(self, coin: Coin) -> str:
|
Development: {data.get('developer_score','Not Available')}
|
||||||
"""Gets market cap for Coin
|
Community: {data.get('community_score','Not Available')}
|
||||||
|
Public Interest: {data.get('public_interest_score','Not Available')}
|
||||||
Parameters
|
"""
|
||||||
----------
|
else:
|
||||||
coin : Coin
|
return f"{symbol.symbol} returned an error."
|
||||||
|
|
||||||
Returns
|
def cap_reply(self, coin: Coin) -> str:
|
||||||
-------
|
"""Gets market cap for Coin
|
||||||
str
|
|
||||||
Preformatted markdown.
|
Parameters
|
||||||
"""
|
----------
|
||||||
|
coin : Coin
|
||||||
if resp := self.get(
|
|
||||||
"/simple/price",
|
Returns
|
||||||
params={
|
-------
|
||||||
"ids": coin.id,
|
str
|
||||||
"vs_currencies": self.vs_currency,
|
Preformatted markdown.
|
||||||
"include_market_cap": "true",
|
"""
|
||||||
},
|
|
||||||
):
|
if resp := self.get(
|
||||||
log.debug(resp)
|
"/simple/price",
|
||||||
try:
|
params={
|
||||||
data = resp[coin.id]
|
"ids": coin.id,
|
||||||
|
"vs_currencies": self.vs_currency,
|
||||||
price = data[self.vs_currency]
|
"include_market_cap": "true",
|
||||||
cap = data[self.vs_currency + "_market_cap"]
|
},
|
||||||
except KeyError:
|
):
|
||||||
return f"{coin.id} returned an error."
|
log.debug(resp)
|
||||||
|
try:
|
||||||
if cap == 0:
|
data = resp[coin.id]
|
||||||
return f"The market cap for {coin.name} is not available for unknown reasons."
|
|
||||||
|
price = data[self.vs_currency]
|
||||||
message = (
|
cap = data[self.vs_currency + "_market_cap"]
|
||||||
f"The current price of {coin.name} is $**{price:,}** and"
|
except KeyError:
|
||||||
+ " its market cap is $**{cap:,.2f}** {self.vs_currency.upper()}"
|
return f"{coin.id} returned an error."
|
||||||
)
|
|
||||||
|
if cap == 0:
|
||||||
else:
|
return f"The market cap for {coin.name} is not available for unknown reasons."
|
||||||
message = f"The Coin: {coin.name} was not found or returned and error."
|
|
||||||
|
message = (
|
||||||
return message
|
f"The current price of {coin.name} is $**{price:,}** and"
|
||||||
|
+ " its market cap is $**{cap:,.2f}** {self.vs_currency.upper()}"
|
||||||
def info_reply(self, symbol: Coin) -> str:
|
)
|
||||||
"""Gets coin description
|
|
||||||
|
else:
|
||||||
Parameters
|
message = f"The Coin: {coin.name} was not found or returned and error."
|
||||||
----------
|
|
||||||
symbol : Coin
|
return message
|
||||||
|
|
||||||
Returns
|
def info_reply(self, symbol: Coin) -> str:
|
||||||
-------
|
"""Gets coin description
|
||||||
str
|
|
||||||
Preformatted markdown.
|
Parameters
|
||||||
"""
|
----------
|
||||||
|
symbol : Coin
|
||||||
if data := self.get(
|
|
||||||
f"/coins/{symbol.id}",
|
Returns
|
||||||
params={"localization": "false"},
|
-------
|
||||||
):
|
str
|
||||||
try:
|
Preformatted markdown.
|
||||||
return markdownify(data["description"]["en"])
|
"""
|
||||||
except KeyError:
|
|
||||||
return f"{symbol} does not have a description available."
|
if data := self.get(
|
||||||
|
f"/coins/{symbol.id}",
|
||||||
return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist."
|
params={"localization": "false"},
|
||||||
|
):
|
||||||
def spark_reply(self, symbol: Coin) -> str:
|
try:
|
||||||
change = self.get(
|
return markdownify(data["description"]["en"])
|
||||||
"/simple/price",
|
except KeyError:
|
||||||
params={
|
return f"{symbol} does not have a description available."
|
||||||
"ids": symbol.id,
|
|
||||||
"vs_currencies": self.vs_currency,
|
return f"No information found for: {symbol}\nEither today is boring or the symbol does not exist."
|
||||||
"include_24hr_change": "true",
|
|
||||||
},
|
def spark_reply(self, symbol: Coin) -> str:
|
||||||
)[symbol.id]["usd_24h_change"]
|
change = self.get(
|
||||||
|
"/simple/price",
|
||||||
return f"`{symbol.tag}`: {symbol.name}, {change:.2f}%"
|
params={
|
||||||
|
"ids": symbol.id,
|
||||||
def trending(self) -> list[str]:
|
"vs_currencies": self.vs_currency,
|
||||||
"""Gets current coins trending on coingecko
|
"include_24hr_change": "true",
|
||||||
|
},
|
||||||
Returns
|
)[symbol.id]["usd_24h_change"]
|
||||||
-------
|
|
||||||
list[str]
|
return f"`{symbol.tag}`: {symbol.name}, {change:.2f}%"
|
||||||
list of $$ID: NAME, CHANGE%
|
|
||||||
"""
|
def trending(self) -> list[str]:
|
||||||
|
"""Gets current coins trending on coingecko
|
||||||
coins = self.get("/search/trending")
|
|
||||||
try:
|
Returns
|
||||||
trending = []
|
-------
|
||||||
for coin in coins["coins"]:
|
list[str]
|
||||||
c = coin["item"]
|
list of $$ID: NAME, CHANGE%
|
||||||
|
"""
|
||||||
sym = c["symbol"].upper()
|
|
||||||
name = c["name"]
|
coins = self.get("/search/trending")
|
||||||
change = self.get(
|
try:
|
||||||
"/simple/price",
|
trending = []
|
||||||
params={
|
for coin in coins["coins"]:
|
||||||
"ids": c["id"],
|
c = coin["item"]
|
||||||
"vs_currencies": self.vs_currency,
|
|
||||||
"include_24hr_change": "true",
|
sym = c["symbol"].upper()
|
||||||
},
|
name = c["name"]
|
||||||
)[c["id"]]["usd_24h_change"]
|
change = self.get(
|
||||||
|
"/simple/price",
|
||||||
msg = f"`$${sym}`: {name}, {change:.2f}%"
|
params={
|
||||||
|
"ids": c["id"],
|
||||||
trending.append(msg)
|
"vs_currencies": self.vs_currency,
|
||||||
|
"include_24hr_change": "true",
|
||||||
except Exception as e:
|
},
|
||||||
log.warning(e)
|
)[c["id"]]["usd_24h_change"]
|
||||||
return self.trending_cache
|
|
||||||
|
msg = f"`$${sym}`: {name}, {change:.2f}%"
|
||||||
self.trending_cache = trending
|
|
||||||
return trending
|
trending.append(msg)
|
||||||
|
|
||||||
def batch_price(self, coins: list[Coin]) -> list[str]:
|
except Exception as e:
|
||||||
"""Gets price of a list of coins all in one API call
|
log.warning(e)
|
||||||
|
return self.trending_cache
|
||||||
Parameters
|
|
||||||
----------
|
self.trending_cache = trending
|
||||||
coins : list[Coin]
|
return trending
|
||||||
|
|
||||||
Returns
|
def batch_price(self, coins: list[Coin]) -> list[str]:
|
||||||
-------
|
"""Gets price of a list of coins all in one API call
|
||||||
list[str]
|
|
||||||
returns preformatted list of strings detailing price movement of each coin passed in.
|
Parameters
|
||||||
"""
|
----------
|
||||||
query = ",".join([c.id for c in coins])
|
coins : list[Coin]
|
||||||
|
|
||||||
prices = self.get(
|
Returns
|
||||||
"/simple/price",
|
-------
|
||||||
params={
|
list[str]
|
||||||
"ids": query,
|
returns preformatted list of strings detailing price movement of each coin passed in.
|
||||||
"vs_currencies": self.vs_currency,
|
"""
|
||||||
"include_24hr_change": "true",
|
query = ",".join([c.id for c in coins])
|
||||||
},
|
|
||||||
)
|
prices = self.get(
|
||||||
|
"/simple/price",
|
||||||
replies = []
|
params={
|
||||||
for coin in coins:
|
"ids": query,
|
||||||
if coin.id in prices:
|
"vs_currencies": self.vs_currency,
|
||||||
p = prices[coin.id]
|
"include_24hr_change": "true",
|
||||||
|
},
|
||||||
if p.get("usd_24h_change") is None:
|
)
|
||||||
p["usd_24h_change"] = 0
|
|
||||||
|
replies = []
|
||||||
replies.append(
|
for coin in coins:
|
||||||
f"{coin.name}: ${p.get('usd',0):,} and has moved {p.get('usd_24h_change',0.0):.2f}% in the past 24 hours."
|
if coin.id in prices:
|
||||||
)
|
p = prices[coin.id]
|
||||||
|
|
||||||
return replies
|
if p.get("usd_24h_change") is None:
|
||||||
|
p["usd_24h_change"] = 0
|
||||||
|
|
||||||
|
replies.append(
|
||||||
|
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
|
||||||
|
@ -1,407 +1,407 @@
|
|||||||
"""Function that routes symbols to the correct API provider.
|
"""Function that routes symbols to the correct API provider.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import schedule
|
import schedule
|
||||||
from cachetools import TTLCache, cached
|
from cachetools import TTLCache, cached
|
||||||
|
|
||||||
from common.cg_Crypto import cg_Crypto
|
from common.cg_Crypto import cg_Crypto
|
||||||
from common.MarketData import MarketData
|
from common.MarketData import MarketData
|
||||||
from common.Symbol import Coin, Stock, Symbol
|
from common.Symbol import Coin, Stock, Symbol
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Router:
|
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})"
|
||||||
trending_count: Dict[str, float] = {}
|
trending_count: Dict[str, float] = {}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.stock = MarketData()
|
self.stock = MarketData()
|
||||||
self.crypto = cg_Crypto()
|
self.crypto = cg_Crypto()
|
||||||
|
|
||||||
schedule.every().hour.do(self.trending_decay)
|
schedule.every().hour.do(self.trending_decay)
|
||||||
|
|
||||||
def trending_decay(self, decay=0.5):
|
def trending_decay(self, decay=0.5):
|
||||||
"""Decays the value of each trending stock by a multiplier"""
|
"""Decays the value of each trending stock by a multiplier"""
|
||||||
t_copy = {}
|
t_copy = {}
|
||||||
dead_keys = []
|
dead_keys = []
|
||||||
if self.trending_count:
|
if self.trending_count:
|
||||||
t_copy = self.trending_count.copy()
|
t_copy = self.trending_count.copy()
|
||||||
for key in t_copy.keys():
|
for key in t_copy.keys():
|
||||||
if t_copy[key] < 0.01:
|
if t_copy[key] < 0.01:
|
||||||
# Prune Keys
|
# Prune Keys
|
||||||
dead_keys.append(key)
|
dead_keys.append(key)
|
||||||
else:
|
else:
|
||||||
t_copy[key] = t_copy[key] * decay
|
t_copy[key] = t_copy[key] * decay
|
||||||
for dead in dead_keys:
|
for dead in dead_keys:
|
||||||
t_copy.pop(dead)
|
t_copy.pop(dead)
|
||||||
|
|
||||||
self.trending_count = t_copy.copy()
|
self.trending_count = t_copy.copy()
|
||||||
log.info("Decayed trending symbols.")
|
log.info("Decayed trending symbols.")
|
||||||
|
|
||||||
def find_symbols(self, text: str, *, trending_weight: int = 1) -> list[Stock | Coin]:
|
def find_symbols(self, text: str, *, trending_weight: int = 1) -> list[Stock | Coin]:
|
||||||
"""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.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
text : str
|
text : str
|
||||||
Blob of text.
|
Blob of text.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
list[Symbol]
|
list[Symbol]
|
||||||
List of stock symbols as Symbol objects
|
List of stock symbols as Symbol objects
|
||||||
"""
|
"""
|
||||||
schedule.run_pending()
|
schedule.run_pending()
|
||||||
|
|
||||||
symbols: list[Symbol] = []
|
symbols: list[Symbol] = []
|
||||||
stock_matches = set(re.findall(self.STOCK_REGEX, text))
|
stock_matches = set(re.findall(self.STOCK_REGEX, text))
|
||||||
coin_matches = set(re.findall(self.CRYPTO_REGEX, text))
|
coin_matches = set(re.findall(self.CRYPTO_REGEX, text))
|
||||||
|
|
||||||
for stock_match in stock_matches:
|
for stock_match in stock_matches:
|
||||||
# Market data lacks tools to check if a symbol is valid.
|
# Market data lacks tools to check if a symbol is valid.
|
||||||
if stock_info := self.stock.symbol_id(stock_match):
|
if stock_info := self.stock.symbol_id(stock_match):
|
||||||
symbols.append(Stock(stock_info))
|
symbols.append(Stock(stock_info))
|
||||||
else:
|
else:
|
||||||
log.info(f"{stock_match} is not in list of stocks")
|
log.info(f"{stock_match} is not in list of stocks")
|
||||||
|
|
||||||
for coin_match in coin_matches:
|
for coin_match in coin_matches:
|
||||||
sym = self.crypto.symbol_list[self.crypto.symbol_list["symbol"].str.fullmatch(coin_match.lower(), case=False)]
|
sym = self.crypto.symbol_list[self.crypto.symbol_list["symbol"].str.fullmatch(coin_match.lower(), case=False)]
|
||||||
if sym.empty:
|
if sym.empty:
|
||||||
log.info(f"{coin_match} is not in list of coins")
|
log.info(f"{coin_match} is not in list of coins")
|
||||||
else:
|
else:
|
||||||
symbols.append(Coin(sym))
|
symbols.append(Coin(sym))
|
||||||
if symbols:
|
if symbols:
|
||||||
for symbol in symbols:
|
for symbol in symbols:
|
||||||
self.trending_count[symbol.tag] = self.trending_count.get(symbol.tag, 0) + trending_weight
|
self.trending_count[symbol.tag] = self.trending_count.get(symbol.tag, 0) + trending_weight
|
||||||
log.debug(self.trending_count)
|
log.debug(self.trending_count)
|
||||||
|
|
||||||
return symbols
|
return symbols
|
||||||
|
|
||||||
def status(self, bot_resp) -> str:
|
def status(self, bot_resp) -> str:
|
||||||
"""Checks for any issues with APIs.
|
"""Checks for any issues with APIs.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
str
|
str
|
||||||
Human readable text on status of the bot and relevant APIs
|
Human readable text on status of the bot and relevant APIs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
stats = f"""
|
stats = f"""
|
||||||
Bot Status:
|
Bot Status:
|
||||||
{bot_resp}
|
{bot_resp}
|
||||||
|
|
||||||
Stock Market Data:
|
Stock Market Data:
|
||||||
{self.stock.status()}
|
{self.stock.status()}
|
||||||
|
|
||||||
Cryptocurrency Data:
|
Cryptocurrency Data:
|
||||||
{self.crypto.status()}
|
{self.crypto.status()}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.warning(stats)
|
log.warning(stats)
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
def inline_search(self, search: str, matches: int = 5) -> pd.DataFrame:
|
def inline_search(self, search: str, matches: int = 5) -> pd.DataFrame:
|
||||||
"""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.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
search : str
|
search : str
|
||||||
String used to match against symbols.
|
String used to match against symbols.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
list[tuple[str, str]]
|
list[tuple[str, str]]
|
||||||
Each tuple contains: (Symbol, Issue Name).
|
Each tuple contains: (Symbol, Issue Name).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# df = pd.concat([self.stock.symbol_list, self.crypto.symbol_list])
|
# df = pd.concat([self.stock.symbol_list, self.crypto.symbol_list])
|
||||||
df = self.crypto.symbol_list
|
df = self.crypto.symbol_list
|
||||||
|
|
||||||
df = df[df["description"].str.contains(search, regex=False, case=False)].sort_values(
|
df = df[df["description"].str.contains(search, regex=False, case=False)].sort_values(
|
||||||
by="type_id", key=lambda x: x.str.len()
|
by="type_id", key=lambda x: x.str.len()
|
||||||
)
|
)
|
||||||
|
|
||||||
symbols = df.head(matches)
|
symbols = df.head(matches)
|
||||||
symbols["price_reply"] = symbols["type_id"].apply(
|
symbols["price_reply"] = symbols["type_id"].apply(
|
||||||
lambda sym: self.price_reply(self.find_symbols(sym, trending_weight=0))[0]
|
lambda sym: self.price_reply(self.find_symbols(sym, trending_weight=0))[0]
|
||||||
)
|
)
|
||||||
|
|
||||||
return symbols
|
return symbols
|
||||||
|
|
||||||
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
|
||||||
----------
|
----------
|
||||||
symbols : list
|
symbols : list
|
||||||
List of stock symbols.
|
List of stock symbols.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
Dict[str, str]
|
Dict[str, str]
|
||||||
Each symbol passed in is a key with its value being a human readable
|
Each symbol passed in is a key with its value being a human readable
|
||||||
markdown formatted string of the symbols price and movement.
|
markdown formatted string of the symbols price and movement.
|
||||||
"""
|
"""
|
||||||
replies = []
|
replies = []
|
||||||
|
|
||||||
for symbol in symbols:
|
for symbol in symbols:
|
||||||
log.info(symbol)
|
log.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:
|
||||||
log.info(f"{symbol} is not a Stock or Coin")
|
log.info(f"{symbol} is not a Stock or Coin")
|
||||||
|
|
||||||
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
|
||||||
-------
|
-------
|
||||||
Dict[str, str]
|
Dict[str, str]
|
||||||
Each symbol passed in is a key with its value being a human readable formatted
|
Each symbol passed in is a key with its value being a human readable formatted
|
||||||
string of the symbols information.
|
string of the symbols information.
|
||||||
"""
|
"""
|
||||||
replies = []
|
replies = []
|
||||||
|
|
||||||
for symbol in symbols:
|
for symbol in symbols:
|
||||||
if isinstance(symbol, Stock):
|
if isinstance(symbol, Stock):
|
||||||
replies.append(self.stock.info_reply(symbol))
|
replies.append(self.stock.info_reply(symbol))
|
||||||
elif isinstance(symbol, Coin):
|
elif isinstance(symbol, Coin):
|
||||||
replies.append(self.crypto.info_reply(symbol))
|
replies.append(self.crypto.info_reply(symbol))
|
||||||
else:
|
else:
|
||||||
log.debug(f"{symbol} is not a Stock or Coin")
|
log.debug(f"{symbol} is not a Stock or Coin")
|
||||||
|
|
||||||
return replies
|
return replies
|
||||||
|
|
||||||
def intra_reply(self, symbol: Symbol) -> pd.DataFrame:
|
def intra_reply(self, symbol: Symbol) -> pd.DataFrame:
|
||||||
"""Returns price data for a symbol since the last market open.
|
"""Returns price data for a symbol since the last market open.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
symbol : str
|
symbol : str
|
||||||
Stock symbol.
|
Stock symbol.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
pd.DataFrame
|
pd.DataFrame
|
||||||
Returns a timeseries dataframe with high, low, and volume data if its available.
|
Returns a timeseries dataframe with high, low, and volume data if its available.
|
||||||
Otherwise returns empty pd.DataFrame.
|
Otherwise returns empty pd.DataFrame.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if isinstance(symbol, Stock):
|
if isinstance(symbol, Stock):
|
||||||
return self.stock.intra_reply(symbol)
|
return self.stock.intra_reply(symbol)
|
||||||
elif isinstance(symbol, Coin):
|
elif isinstance(symbol, Coin):
|
||||||
return self.crypto.intra_reply(symbol)
|
return self.crypto.intra_reply(symbol)
|
||||||
else:
|
else:
|
||||||
log.debug(f"{symbol} is not a Stock or Coin")
|
log.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:
|
||||||
"""Returns price data for a symbol of the past month up until the previous trading days close.
|
"""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.
|
Also caches multiple requests made in the same day.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
symbol : str
|
symbol : str
|
||||||
Stock symbol.
|
Stock symbol.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
pd.DataFrame
|
pd.DataFrame
|
||||||
Returns a timeseries dataframe with high, low, and volume data if its available.
|
Returns a timeseries dataframe with high, low, and volume data if its available.
|
||||||
Otherwise returns empty pd.DataFrame.
|
Otherwise returns empty pd.DataFrame.
|
||||||
"""
|
"""
|
||||||
if isinstance(symbol, Stock):
|
if isinstance(symbol, Stock):
|
||||||
return self.stock.chart_reply(symbol)
|
return self.stock.chart_reply(symbol)
|
||||||
elif isinstance(symbol, Coin):
|
elif isinstance(symbol, Coin):
|
||||||
return self.crypto.chart_reply(symbol)
|
return self.crypto.chart_reply(symbol)
|
||||||
else:
|
else:
|
||||||
log.debug(f"{symbol} is not a Stock or Coin")
|
log.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]:
|
||||||
"""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
|
||||||
-------
|
-------
|
||||||
Dict[str, str]
|
Dict[str, str]
|
||||||
Each symbol passed in is a key with its value being a human readable
|
Each symbol passed in is a key with its value being a human readable
|
||||||
formatted string of the symbols statistics.
|
formatted string of the symbols statistics.
|
||||||
"""
|
"""
|
||||||
replies = []
|
replies = []
|
||||||
|
|
||||||
for symbol in symbols:
|
for symbol in symbols:
|
||||||
if isinstance(symbol, Stock):
|
if isinstance(symbol, Stock):
|
||||||
replies.append(self.stock.stat_reply(symbol))
|
replies.append(self.stock.stat_reply(symbol))
|
||||||
elif isinstance(symbol, Coin):
|
elif isinstance(symbol, Coin):
|
||||||
replies.append(self.crypto.stat_reply(symbol))
|
replies.append(self.crypto.stat_reply(symbol))
|
||||||
else:
|
else:
|
||||||
log.debug(f"{symbol} is not a Stock or Coin")
|
log.debug(f"{symbol} is not a Stock or Coin")
|
||||||
|
|
||||||
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
|
||||||
-------
|
-------
|
||||||
Dict[str, str]
|
Dict[str, str]
|
||||||
Each symbol passed in is a key with its value being a human readable
|
Each symbol passed in is a key with its value being a human readable
|
||||||
formatted string of the symbols market cap.
|
formatted string of the symbols market cap.
|
||||||
"""
|
"""
|
||||||
replies = []
|
replies = []
|
||||||
|
|
||||||
for symbol in symbols:
|
for symbol in symbols:
|
||||||
if isinstance(symbol, Stock):
|
if isinstance(symbol, Stock):
|
||||||
replies.append(self.stock.cap_reply(symbol))
|
replies.append(self.stock.cap_reply(symbol))
|
||||||
elif isinstance(symbol, Coin):
|
elif isinstance(symbol, Coin):
|
||||||
replies.append(self.crypto.cap_reply(symbol))
|
replies.append(self.crypto.cap_reply(symbol))
|
||||||
else:
|
else:
|
||||||
log.debug(f"{symbol} is not a Stock or Coin")
|
log.debug(f"{symbol} is not a Stock or Coin")
|
||||||
|
|
||||||
return replies
|
return replies
|
||||||
|
|
||||||
def spark_reply(self, symbols: list[Symbol]) -> list[str]:
|
def spark_reply(self, symbols: list[Symbol]) -> list[str]:
|
||||||
"""Gets change for each symbol and returns it in a compact format
|
"""Gets change for each symbol and returns it in a compact format
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
symbols : list[str]
|
symbols : list[str]
|
||||||
List of stock symbols
|
List of stock symbols
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
list[str]
|
list[str]
|
||||||
List of human readable strings.
|
List of human readable strings.
|
||||||
"""
|
"""
|
||||||
replies = []
|
replies = []
|
||||||
|
|
||||||
for symbol in symbols:
|
for symbol in symbols:
|
||||||
if isinstance(symbol, Stock):
|
if isinstance(symbol, Stock):
|
||||||
replies.append(self.stock.spark_reply(symbol))
|
replies.append(self.stock.spark_reply(symbol))
|
||||||
elif isinstance(symbol, Coin):
|
elif isinstance(symbol, Coin):
|
||||||
replies.append(self.crypto.spark_reply(symbol))
|
replies.append(self.crypto.spark_reply(symbol))
|
||||||
else:
|
else:
|
||||||
log.debug(f"{symbol} is not a Stock or Coin")
|
log.debug(f"{symbol} is not a Stock or Coin")
|
||||||
|
|
||||||
return replies
|
return replies
|
||||||
|
|
||||||
@cached(cache=TTLCache(maxsize=1024, ttl=600))
|
@cached(cache=TTLCache(maxsize=1024, ttl=600))
|
||||||
def trending(self) -> str:
|
def trending(self) -> str:
|
||||||
"""Checks APIs for trending symbols.
|
"""Checks APIs for trending symbols.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
list[str]
|
list[str]
|
||||||
List of preformatted strings to be sent to user.
|
List of preformatted strings to be sent to user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# stocks = self.stock.trending()
|
# stocks = self.stock.trending()
|
||||||
coins = self.crypto.trending()
|
coins = self.crypto.trending()
|
||||||
|
|
||||||
reply = ""
|
reply = ""
|
||||||
|
|
||||||
log.warning(self.trending_count)
|
log.warning(self.trending_count)
|
||||||
if self.trending_count:
|
if self.trending_count:
|
||||||
reply += "🔥Trending on the Stock Bot:\n`"
|
reply += "🔥Trending on the Stock Bot:\n`"
|
||||||
reply += "━" * len("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]
|
sorted_trending = [s[0] for s in sorted(self.trending_count.items(), key=lambda item: item[1])][::-1][0:5]
|
||||||
log.warning(sorted_trending)
|
log.warning(sorted_trending)
|
||||||
for t in sorted_trending:
|
for t in sorted_trending:
|
||||||
reply += self.spark_reply(self.find_symbols(t))[0] + "\n"
|
reply += self.spark_reply(self.find_symbols(t))[0] + "\n"
|
||||||
|
|
||||||
if coins:
|
if coins:
|
||||||
reply += "\n\n🦎Trending on CoinGecko:\n`"
|
reply += "\n\n🦎Trending on CoinGecko:\n`"
|
||||||
reply += "━" * len("Trending on CoinGecko:") + "`\n"
|
reply += "━" * len("Trending on CoinGecko:") + "`\n"
|
||||||
for coin in coins:
|
for coin in coins:
|
||||||
reply += coin + "\n"
|
reply += coin + "\n"
|
||||||
|
|
||||||
if "`$GME" in reply:
|
if "`$GME" in reply:
|
||||||
reply = reply.replace("🔥", "🦍")
|
reply = reply.replace("🔥", "🦍")
|
||||||
|
|
||||||
if reply:
|
if reply:
|
||||||
return reply
|
return reply
|
||||||
else:
|
else:
|
||||||
log.warning("Failed to collect trending data.")
|
log.warning("Failed to collect trending data.")
|
||||||
return "Trending data is not currently available."
|
return "Trending data is not currently available."
|
||||||
|
|
||||||
def random_pick(self) -> str:
|
def random_pick(self) -> str:
|
||||||
# choice = random.choice(list(self.stock.symbol_list["description"]) + list(self.crypto.symbol_list["description"]))
|
# choice = random.choice(list(self.stock.symbol_list["description"]) + list(self.crypto.symbol_list["description"]))
|
||||||
choice = random.choice(list(self.crypto.symbol_list["description"]))
|
choice = random.choice(list(self.crypto.symbol_list["description"]))
|
||||||
hold = (datetime.date.today() + datetime.timedelta(random.randint(1, 365))).strftime("%b %d, %Y")
|
hold = (datetime.date.today() + datetime.timedelta(random.randint(1, 365))).strftime("%b %d, %Y")
|
||||||
|
|
||||||
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
|
||||||
----------
|
----------
|
||||||
symbols : list
|
symbols : list
|
||||||
List of stock symbols.
|
List of stock symbols.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
Dict[str, str]
|
Dict[str, str]
|
||||||
Each symbol passed in is a key with its value being a human readable
|
Each symbol passed in is a key with its value being a human readable
|
||||||
markdown formatted string of the symbols price and movement.
|
markdown formatted string of the symbols price and movement.
|
||||||
"""
|
"""
|
||||||
replies = []
|
replies = []
|
||||||
stocks = []
|
stocks = []
|
||||||
coins = []
|
coins = []
|
||||||
|
|
||||||
for symbol in symbols:
|
for symbol in symbols:
|
||||||
if isinstance(symbol, Stock):
|
if isinstance(symbol, Stock):
|
||||||
stocks.append(symbol)
|
stocks.append(symbol)
|
||||||
elif isinstance(symbol, Coin):
|
elif isinstance(symbol, Coin):
|
||||||
coins.append(symbol)
|
coins.append(symbol)
|
||||||
else:
|
else:
|
||||||
log.debug(f"{symbol} is not a Stock or Coin")
|
log.debug(f"{symbol} is not a Stock or Coin")
|
||||||
|
|
||||||
if stocks:
|
if stocks:
|
||||||
for stock in stocks:
|
for stock in stocks:
|
||||||
replies.append(self.stock.price_reply(stock))
|
replies.append(self.stock.price_reply(stock))
|
||||||
if coins:
|
if coins:
|
||||||
replies = replies + self.crypto.batch_price(coins)
|
replies = replies + self.crypto.batch_price(coins)
|
||||||
|
|
||||||
return replies
|
return replies
|
||||||
|
|
||||||
def options(self, request: str, symbols: list[Symbol]) -> Dict:
|
def options(self, request: str, symbols: list[Symbol]) -> Dict:
|
||||||
request = request.lower()
|
request = request.lower()
|
||||||
if len(symbols) == 1:
|
if len(symbols) == 1:
|
||||||
symbol = symbols[0]
|
symbol = symbols[0]
|
||||||
request = request.replace(symbol.tag.lower(), symbol.symbol.lower())
|
request = request.replace(symbol.tag.lower(), symbol.symbol.lower())
|
||||||
return self.stock.options_reply(request)
|
return self.stock.options_reply(request)
|
||||||
else:
|
else:
|
||||||
return self.stock.options_reply(request)
|
return self.stock.options_reply(request)
|
||||||
|
@ -1,31 +1,31 @@
|
|||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def rate_limited(max_per_second):
|
def rate_limited(max_per_second):
|
||||||
"""
|
"""
|
||||||
Decorator that ensures the wrapped function is called at most `max_per_second` times per second.
|
Decorator that ensures the wrapped function is called at most `max_per_second` times per second.
|
||||||
"""
|
"""
|
||||||
min_interval = 1.0 / max_per_second
|
min_interval = 1.0 / max_per_second
|
||||||
|
|
||||||
def decorate(func):
|
def decorate(func):
|
||||||
last_called = [0.0]
|
last_called = [0.0]
|
||||||
|
|
||||||
def rate_limited_function(*args, **kwargs):
|
def rate_limited_function(*args, **kwargs):
|
||||||
elapsed = time.time() - last_called[0]
|
elapsed = time.time() - last_called[0]
|
||||||
left_to_wait = min_interval - elapsed
|
left_to_wait = min_interval - elapsed
|
||||||
|
|
||||||
if left_to_wait > 0:
|
if left_to_wait > 0:
|
||||||
log.info(f"Rate limit exceeded. Waiting for {left_to_wait:.2f} seconds.")
|
log.info(f"Rate limit exceeded. Waiting for {left_to_wait:.2f} seconds.")
|
||||||
time.sleep(left_to_wait)
|
time.sleep(left_to_wait)
|
||||||
|
|
||||||
ret = func(*args, **kwargs)
|
ret = func(*args, **kwargs)
|
||||||
last_called[0] = time.time()
|
last_called[0] = time.time()
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
return rate_limited_function
|
return rate_limited_function
|
||||||
|
|
||||||
return decorate
|
return decorate
|
||||||
|
@ -1,59 +1,59 @@
|
|||||||
"""Functions and Info specific to the discord Bot
|
"""Functions and Info specific to the discord Bot
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import requests as r
|
import requests as r
|
||||||
|
|
||||||
|
|
||||||
class D_info:
|
class D_info:
|
||||||
license = re.sub(
|
license = re.sub(
|
||||||
r"\b\n",
|
r"\b\n",
|
||||||
" ",
|
" ",
|
||||||
r.get("https://gitlab.com/simple-stock-bots/simple-stock-bot/-/raw/master/LICENSE").text,
|
r.get("https://gitlab.com/simple-stock-bots/simple-stock-bot/-/raw/master/LICENSE").text,
|
||||||
)
|
)
|
||||||
|
|
||||||
help_text = """
|
help_text = """
|
||||||
Thanks for using this bot. If you like it, [support me with a beer](https://www.buymeacoffee.com/Anson). 🍻
|
Thanks for using this bot. If you like it, [support me with a beer](https://www.buymeacoffee.com/Anson). 🍻
|
||||||
|
|
||||||
For stock data or hosting your own bot, use my link. This helps keep the bot free:
|
For stock data or hosting your own bot, use my link. This helps keep the bot free:
|
||||||
[marketdata.app](https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=discord).
|
[marketdata.app](https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=discord).
|
||||||
|
|
||||||
**Updates**: Join the bot's discord: https://t.me/simplestockbotnews.
|
**Updates**: Join the bot's discord: https://t.me/simplestockbotnews.
|
||||||
|
|
||||||
**Documentation**: All details about the bot are at [docs](https://simplestockbot.com).
|
**Documentation**: All details about the bot are at [docs](https://simplestockbot.com).
|
||||||
|
|
||||||
The bot reads _"Symbols"_. Use `$` for stock tickers and `$$` for cryptocurrencies. For example:
|
The bot reads _"Symbols"_. Use `$` for stock tickers and `$$` for cryptocurrencies. For example:
|
||||||
- `/chart $$eth` gives Ethereum's monthly chart.
|
- `/chart $$eth` gives Ethereum's monthly chart.
|
||||||
- `/dividend $psec` shows Prospect Capital's dividend.
|
- `/dividend $psec` shows Prospect Capital's dividend.
|
||||||
|
|
||||||
Type any symbol, and the bot shows its price. Like: `Is $$btc rising since $tsla accepts it?` will give Bitcoin and Tesla prices.
|
Type any symbol, and the bot shows its price. Like: `Is $$btc rising since $tsla accepts it?` will give Bitcoin and Tesla prices.
|
||||||
|
|
||||||
**Commands**
|
**Commands**
|
||||||
- `/donate [USD amount]`: Support the bot. 🎗️
|
- `/donate [USD amount]`: Support the bot. 🎗️
|
||||||
- `/intra $[symbol]`: See stock's latest movement. 📈
|
- `/intra $[symbol]`: See stock's latest movement. 📈
|
||||||
- `/chart $[symbol]`: View a month's stock activity. 📊
|
- `/chart $[symbol]`: View a month's stock activity. 📊
|
||||||
- `/trending`: Check trending stocks and cryptos. 💬
|
- `/trending`: Check trending stocks and cryptos. 💬
|
||||||
- `/help`: Need help? Ask here. 🆘
|
- `/help`: Need help? Ask here. 🆘
|
||||||
|
|
||||||
**Inline Features**
|
**Inline Features**
|
||||||
Type @SimpleStockBot `[search]` anywhere to find and get stock/crypto prices. Note: Prices might be delayed up to an hour.
|
Type @SimpleStockBot `[search]` anywhere to find and get stock/crypto prices. Note: Prices might be delayed up to an hour.
|
||||||
|
|
||||||
Data from: [marketdata.app](https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=discord).
|
Data from: [marketdata.app](https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=discord).
|
||||||
|
|
||||||
Issues with the bot? Use `/status` or [contact us](https://simplestockbot.com/contact).
|
Issues with the bot? Use `/status` or [contact us](https://simplestockbot.com/contact).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
donate_text = """
|
donate_text = """
|
||||||
Simple Stock Bot runs purely on [donations.](https://www.buymeacoffee.com/Anson)
|
Simple Stock Bot runs purely on [donations.](https://www.buymeacoffee.com/Anson)
|
||||||
Every donation supports server costs and
|
Every donation supports server costs and
|
||||||
[marketdata.app](https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=discord) provides our data.
|
[marketdata.app](https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=discord) provides our data.
|
||||||
|
|
||||||
**How to Donate?**
|
**How to Donate?**
|
||||||
1. Use `/donate [amount in USD]` command.
|
1. Use `/donate [amount in USD]` command.
|
||||||
- E.g., `/donate 2` donates 2 USD.
|
- E.g., `/donate 2` donates 2 USD.
|
||||||
2. Or, donate at [buymeacoffee](https://www.buymeacoffee.com/Anson).
|
2. Or, donate at [buymeacoffee](https://www.buymeacoffee.com/Anson).
|
||||||
- It's quick, doesn't need an account, and accepts Paypal or Credit card.
|
- It's quick, doesn't need an account, and accepts Paypal or Credit card.
|
||||||
|
|
||||||
Questions? Visit our [website](https://simplestockbot.com).
|
Questions? Visit our [website](https://simplestockbot.com).
|
||||||
"""
|
"""
|
||||||
|
512
discord/bot.py
512
discord/bot.py
@ -1,256 +1,256 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import mplfinance as mpf
|
import mplfinance as mpf
|
||||||
import nextcord
|
import nextcord
|
||||||
from D_info import D_info
|
from D_info import D_info
|
||||||
from nextcord.ext import commands
|
from nextcord.ext import commands
|
||||||
|
|
||||||
from common.symbol_router import Router
|
from common.symbol_router import Router
|
||||||
|
|
||||||
DISCORD_TOKEN = os.environ["DISCORD"]
|
DISCORD_TOKEN = os.environ["DISCORD"]
|
||||||
|
|
||||||
s = Router()
|
s = Router()
|
||||||
d = D_info()
|
d = D_info()
|
||||||
|
|
||||||
|
|
||||||
intents = nextcord.Intents.default()
|
intents = nextcord.Intents.default()
|
||||||
|
|
||||||
|
|
||||||
client = nextcord.Client(intents=intents)
|
client = nextcord.Client(intents=intents)
|
||||||
bot = commands.Bot(command_prefix="/", description=d.help_text, intents=intents)
|
bot = commands.Bot(command_prefix="/", description=d.help_text, intents=intents)
|
||||||
|
|
||||||
logger = logging.getLogger("nextcord")
|
logger = logging.getLogger("nextcord")
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
handler = logging.FileHandler(filename="nextcord.log", encoding="utf-8", mode="w")
|
handler = logging.FileHandler(filename="nextcord.log", encoding="utf-8", mode="w")
|
||||||
handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
|
handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_ready():
|
async def on_ready():
|
||||||
logging.info("Starting Simple Stock Bot")
|
logging.info("Starting Simple Stock Bot")
|
||||||
logging.info(f"Logged in as {bot.user.name} {bot.user.id}")
|
logging.info(f"Logged in as {bot.user.name} {bot.user.id}")
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def status(ctx: commands):
|
async def status(ctx: commands):
|
||||||
"""Debug command for diagnosing if the bot is experiencing any issues."""
|
"""Debug command for diagnosing if the bot is experiencing any issues."""
|
||||||
logging.info(f"Status command ran by {ctx.message.author}")
|
logging.info(f"Status command ran by {ctx.message.author}")
|
||||||
message = ""
|
message = ""
|
||||||
try:
|
try:
|
||||||
message = "Contact MisterBiggs#0465 if you need help.\n"
|
message = "Contact MisterBiggs#0465 if you need help.\n"
|
||||||
message += s.status(f"Bot recieved your message in: {bot.latency*10:.4f} seconds") + "\n"
|
message += s.status(f"Bot recieved your message in: {bot.latency*10:.4f} seconds") + "\n"
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logging.critical(ex)
|
logging.critical(ex)
|
||||||
message += (
|
message += (
|
||||||
f"*\n\nERROR ENCOUNTERED:*\n{ex}\n\n"
|
f"*\n\nERROR ENCOUNTERED:*\n{ex}\n\n"
|
||||||
+ "*The bot encountered an error while attempting to find errors. Please contact the bot admin.*"
|
+ "*The bot encountered an error while attempting to find errors. Please contact the bot admin.*"
|
||||||
)
|
)
|
||||||
await ctx.send(message)
|
await ctx.send(message)
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def license(ctx: commands):
|
async def license(ctx: commands):
|
||||||
"""Returns the bots license agreement."""
|
"""Returns the bots license agreement."""
|
||||||
await ctx.send(d.license)
|
await ctx.send(d.license)
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def donate(ctx: commands):
|
async def donate(ctx: commands):
|
||||||
"""Details on how to support the development and hosting of the bot."""
|
"""Details on how to support the development and hosting of the bot."""
|
||||||
await ctx.send(d.donate_text)
|
await ctx.send(d.donate_text)
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def search(ctx: commands, *, query: str):
|
async def search(ctx: commands, *, query: str):
|
||||||
"""Search for a stock symbol using either symbol of company name."""
|
"""Search for a stock symbol using either symbol of company name."""
|
||||||
results = s.search_symbols(query)
|
results = s.search_symbols(query)
|
||||||
if results:
|
if results:
|
||||||
reply = "*Search Results:*\n`$ticker: Company Name`\n"
|
reply = "*Search Results:*\n`$ticker: Company Name`\n"
|
||||||
for query in results:
|
for query in results:
|
||||||
reply += "`" + query[1] + "`\n"
|
reply += "`" + query[1] + "`\n"
|
||||||
await ctx.send(reply)
|
await ctx.send(reply)
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def crypto(ctx: commands, _: str):
|
async def crypto(ctx: commands, _: str):
|
||||||
"""Get the price of a cryptocurrency using in USD."""
|
"""Get the price of a cryptocurrency using in USD."""
|
||||||
await ctx.send("Crypto now has native support. Any crypto can be called using two dollar signs: `$$eth` `$$btc` `$$doge`")
|
await ctx.send("Crypto now has native support. Any crypto can be called using two dollar signs: `$$eth` `$$btc` `$$doge`")
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def intra(ctx: commands, sym: str):
|
async def intra(ctx: commands, sym: str):
|
||||||
"""Get a chart for the stocks movement since market open."""
|
"""Get a chart for the stocks movement since market open."""
|
||||||
symbols = s.find_symbols(sym)
|
symbols = s.find_symbols(sym)
|
||||||
|
|
||||||
if len(symbols):
|
if len(symbols):
|
||||||
symbol = symbols[0]
|
symbol = symbols[0]
|
||||||
else:
|
else:
|
||||||
await ctx.send("No symbols or coins found.")
|
await ctx.send("No symbols or coins found.")
|
||||||
return
|
return
|
||||||
|
|
||||||
df = s.intra_reply(symbol)
|
df = s.intra_reply(symbol)
|
||||||
if df.empty:
|
if df.empty:
|
||||||
await ctx.send("Invalid symbol please see `/help` for usage details.")
|
await ctx.send("Invalid symbol please see `/help` for usage details.")
|
||||||
return
|
return
|
||||||
with ctx.channel.typing():
|
with ctx.channel.typing():
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
mpf.plot(
|
mpf.plot(
|
||||||
df,
|
df,
|
||||||
type="renko",
|
type="renko",
|
||||||
title=f"\n{symbol.name}",
|
title=f"\n{symbol.name}",
|
||||||
volume="volume" in df.keys(),
|
volume="volume" in df.keys(),
|
||||||
style="yahoo",
|
style="yahoo",
|
||||||
savefig=dict(fname=buf, dpi=400, bbox_inches="tight"),
|
savefig=dict(fname=buf, dpi=400, bbox_inches="tight"),
|
||||||
)
|
)
|
||||||
|
|
||||||
buf.seek(0)
|
buf.seek(0)
|
||||||
|
|
||||||
# Get price so theres no request lag after the image is sent
|
# Get price so theres no request lag after the image is sent
|
||||||
price_reply = s.price_reply([symbol])[0]
|
price_reply = s.price_reply([symbol])[0]
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
file=nextcord.File(
|
file=nextcord.File(
|
||||||
buf,
|
buf,
|
||||||
filename=f"{symbol.name}:intra{datetime.date.today().strftime('%S%M%d%b%Y')}.png",
|
filename=f"{symbol.name}:intra{datetime.date.today().strftime('%S%M%d%b%Y')}.png",
|
||||||
),
|
),
|
||||||
content=f"\nIntraday chart for {symbol.name} from {df.first_valid_index().strftime('%d %b at %H:%M')} to"
|
content=f"\nIntraday chart for {symbol.name} from {df.first_valid_index().strftime('%d %b at %H:%M')} to"
|
||||||
+ f" {df.last_valid_index().strftime('%d %b at %H:%M')}",
|
+ f" {df.last_valid_index().strftime('%d %b at %H:%M')}",
|
||||||
)
|
)
|
||||||
await ctx.send(price_reply)
|
await ctx.send(price_reply)
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def chart(ctx: commands, sym: str):
|
async def chart(ctx: commands, sym: str):
|
||||||
"""returns a chart of the past month of data for a symbol"""
|
"""returns a chart of the past month of data for a symbol"""
|
||||||
|
|
||||||
symbols = s.find_symbols(sym)
|
symbols = s.find_symbols(sym)
|
||||||
|
|
||||||
if len(symbols):
|
if len(symbols):
|
||||||
symbol = symbols[0]
|
symbol = symbols[0]
|
||||||
else:
|
else:
|
||||||
await ctx.send("No symbols or coins found.")
|
await ctx.send("No symbols or coins found.")
|
||||||
return
|
return
|
||||||
|
|
||||||
df = s.chart_reply(symbol)
|
df = s.chart_reply(symbol)
|
||||||
if df.empty:
|
if df.empty:
|
||||||
await ctx.send("Invalid symbol please see `/help` for usage details.")
|
await ctx.send("Invalid symbol please see `/help` for usage details.")
|
||||||
return
|
return
|
||||||
with ctx.channel.typing():
|
with ctx.channel.typing():
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
mpf.plot(
|
mpf.plot(
|
||||||
df,
|
df,
|
||||||
type="candle",
|
type="candle",
|
||||||
title=f"\n{symbol.name}",
|
title=f"\n{symbol.name}",
|
||||||
volume="volume" in df.keys(),
|
volume="volume" in df.keys(),
|
||||||
style="yahoo",
|
style="yahoo",
|
||||||
savefig=dict(fname=buf, dpi=400, bbox_inches="tight"),
|
savefig=dict(fname=buf, dpi=400, bbox_inches="tight"),
|
||||||
)
|
)
|
||||||
buf.seek(0)
|
buf.seek(0)
|
||||||
|
|
||||||
# Get price so theres no request lag after the image is sent
|
# Get price so theres no request lag after the image is sent
|
||||||
price_reply = s.price_reply([symbol])[0]
|
price_reply = s.price_reply([symbol])[0]
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
file=nextcord.File(
|
file=nextcord.File(
|
||||||
buf,
|
buf,
|
||||||
filename=f"{symbol.name}:1M{datetime.date.today().strftime('%d%b%Y')}.png",
|
filename=f"{symbol.name}:1M{datetime.date.today().strftime('%d%b%Y')}.png",
|
||||||
),
|
),
|
||||||
content=f"\n1 Month chart for {symbol.name} from {df.first_valid_index().strftime('%d, %b %Y')}"
|
content=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')}",
|
+ f" to {df.last_valid_index().strftime('%d, %b %Y')}",
|
||||||
)
|
)
|
||||||
await ctx.send(price_reply)
|
await ctx.send(price_reply)
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def cap(ctx: commands, sym: str):
|
async def cap(ctx: commands, sym: str):
|
||||||
"""Get the market cap of a symbol"""
|
"""Get the market cap of a symbol"""
|
||||||
symbols = s.find_symbols(sym)
|
symbols = s.find_symbols(sym)
|
||||||
if symbols:
|
if symbols:
|
||||||
with ctx.channel.typing():
|
with ctx.channel.typing():
|
||||||
for reply in s.cap_reply(symbols):
|
for reply in s.cap_reply(symbols):
|
||||||
await ctx.send(reply)
|
await ctx.send(reply)
|
||||||
|
|
||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def trending(ctx: commands):
|
async def trending(ctx: commands):
|
||||||
"""Get a list of Trending Stocks and Coins"""
|
"""Get a list of Trending Stocks and Coins"""
|
||||||
with ctx.channel.typing():
|
with ctx.channel.typing():
|
||||||
await ctx.send(s.trending())
|
await ctx.send(s.trending())
|
||||||
|
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_message(message):
|
async def on_message(message):
|
||||||
# Ignore messages from the bot itself
|
# Ignore messages from the bot itself
|
||||||
if message.author.id == bot.user.id:
|
if message.author.id == bot.user.id:
|
||||||
return
|
return
|
||||||
|
|
||||||
content_lower = message.content.lower()
|
content_lower = message.content.lower()
|
||||||
|
|
||||||
# Process commands starting with "/"
|
# Process commands starting with "/"
|
||||||
if message.content.startswith("/"):
|
if message.content.startswith("/"):
|
||||||
await bot.process_commands(message)
|
await bot.process_commands(message)
|
||||||
return
|
return
|
||||||
|
|
||||||
symbols = None
|
symbols = None
|
||||||
if "$" in message.content:
|
if "$" in message.content:
|
||||||
symbols = s.find_symbols(message.content)
|
symbols = s.find_symbols(message.content)
|
||||||
|
|
||||||
if "call" in content_lower or "put" in content_lower:
|
if "call" in content_lower or "put" in content_lower:
|
||||||
await handle_options(message, symbols)
|
await handle_options(message, symbols)
|
||||||
return
|
return
|
||||||
|
|
||||||
if symbols:
|
if symbols:
|
||||||
for reply in s.price_reply(symbols):
|
for reply in s.price_reply(symbols):
|
||||||
await message.channel.send(reply)
|
await message.channel.send(reply)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
async def handle_options(message, symbols):
|
async def handle_options(message, symbols):
|
||||||
logging.info("Options detected")
|
logging.info("Options detected")
|
||||||
try:
|
try:
|
||||||
options_data = s.options(message.content.lower(), symbols)
|
options_data = s.options(message.content.lower(), symbols)
|
||||||
|
|
||||||
# Create the embed directly within the function
|
# Create the embed directly within the function
|
||||||
embed = nextcord.Embed(title=options_data["Option Symbol"], description=options_data["Underlying"], color=0x3498DB)
|
embed = nextcord.Embed(title=options_data["Option Symbol"], description=options_data["Underlying"], color=0x3498DB)
|
||||||
|
|
||||||
# Key details
|
# Key details
|
||||||
details = (
|
details = (
|
||||||
f"Expiration: {options_data['Expiration']}\n" f"Side: {options_data['side']}\n" f"Strike: {options_data['strike']}"
|
f"Expiration: {options_data['Expiration']}\n" f"Side: {options_data['side']}\n" f"Strike: {options_data['strike']}"
|
||||||
)
|
)
|
||||||
embed.add_field(name="Details", value=details, inline=False)
|
embed.add_field(name="Details", value=details, inline=False)
|
||||||
|
|
||||||
# Pricing info
|
# Pricing info
|
||||||
pricing_info = (
|
pricing_info = (
|
||||||
f"Bid: {options_data['bid']} (Size: {options_data['bidSize']})\n"
|
f"Bid: {options_data['bid']} (Size: {options_data['bidSize']})\n"
|
||||||
f"Mid: {options_data['mid']}\n"
|
f"Mid: {options_data['mid']}\n"
|
||||||
f"Ask: {options_data['ask']} (Size: {options_data['askSize']})\n"
|
f"Ask: {options_data['ask']} (Size: {options_data['askSize']})\n"
|
||||||
f"Last: {options_data['last']}"
|
f"Last: {options_data['last']}"
|
||||||
)
|
)
|
||||||
embed.add_field(name="Pricing", value=pricing_info, inline=False)
|
embed.add_field(name="Pricing", value=pricing_info, inline=False)
|
||||||
|
|
||||||
# Volume and open interest
|
# Volume and open interest
|
||||||
volume_info = f"Open Interest: {options_data['Open Interest']}\n" f"Volume: {options_data['Volume']}"
|
volume_info = f"Open Interest: {options_data['Open Interest']}\n" f"Volume: {options_data['Volume']}"
|
||||||
embed.add_field(name="Activity", value=volume_info, inline=False)
|
embed.add_field(name="Activity", value=volume_info, inline=False)
|
||||||
|
|
||||||
# Greeks
|
# Greeks
|
||||||
greeks_info = (
|
greeks_info = (
|
||||||
f"IV: {options_data['Implied Volatility']}\n"
|
f"IV: {options_data['Implied Volatility']}\n"
|
||||||
f"Delta: {options_data['delta']}\n"
|
f"Delta: {options_data['delta']}\n"
|
||||||
f"Gamma: {options_data['gamma']}\n"
|
f"Gamma: {options_data['gamma']}\n"
|
||||||
f"Theta: {options_data['theta']}\n"
|
f"Theta: {options_data['theta']}\n"
|
||||||
f"Vega: {options_data['vega']}\n"
|
f"Vega: {options_data['vega']}\n"
|
||||||
f"Rho: {options_data['rho']}"
|
f"Rho: {options_data['rho']}"
|
||||||
)
|
)
|
||||||
embed.add_field(name="Greeks", value=greeks_info, inline=False)
|
embed.add_field(name="Greeks", value=greeks_info, inline=False)
|
||||||
|
|
||||||
# Send the created embed
|
# Send the created embed
|
||||||
await message.channel.send(embed=embed)
|
await message.channel.send(embed=embed)
|
||||||
|
|
||||||
except KeyError as ex:
|
except KeyError as ex:
|
||||||
logging.warning(f"KeyError processing options for message {message.content}: {ex}")
|
logging.warning(f"KeyError processing options for message {message.content}: {ex}")
|
||||||
|
|
||||||
|
|
||||||
bot.run(DISCORD_TOKEN)
|
bot.run(DISCORD_TOKEN)
|
||||||
|
@ -1,11 +1,2 @@
|
|||||||
[tool.black]
|
|
||||||
line-length = 130
|
|
||||||
|
|
||||||
[tool.flake8]
|
|
||||||
max-line-length = 130
|
|
||||||
|
|
||||||
[tool.pycodestyle]
|
|
||||||
max_line_length = 130
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 130
|
line-length = 130
|
@ -1,70 +1,70 @@
|
|||||||
"""Functions and Info specific to the Telegram Bot
|
"""Functions and Info specific to the Telegram Bot
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import requests as r
|
import requests as r
|
||||||
|
|
||||||
|
|
||||||
class T_info:
|
class T_info:
|
||||||
license = re.sub(
|
license = re.sub(
|
||||||
r"\b\n",
|
r"\b\n",
|
||||||
" ",
|
" ",
|
||||||
r.get("https://gitlab.com/simple-stock-bots/simple-stock-bot/-/raw/master/LICENSE").text,
|
r.get("https://gitlab.com/simple-stock-bots/simple-stock-bot/-/raw/master/LICENSE").text,
|
||||||
)
|
)
|
||||||
|
|
||||||
help_text = """
|
help_text = """
|
||||||
Appreciate this bot? Show support by [buying me a beer](https://www.buymeacoffee.com/Anson) 🍻.
|
Appreciate this bot? Show support by [buying me a beer](https://www.buymeacoffee.com/Anson) 🍻.
|
||||||
|
|
||||||
Want stock data or to host your own bot? Help keep this bot free by using my
|
Want stock data or to host your own bot? Help keep this bot free by using my
|
||||||
[affiliate link](https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=telegram).
|
[affiliate link](https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=telegram).
|
||||||
|
|
||||||
📢 Stay updated on the bot's Telegram: https://t.me/simplestockbotnews.
|
📢 Stay updated on the bot's Telegram: https://t.me/simplestockbotnews.
|
||||||
|
|
||||||
**Guide**: All about using and setting up the bot is in the [docs](https://simplestockbot.com).
|
**Guide**: All about using and setting up the bot is in the [docs](https://simplestockbot.com).
|
||||||
|
|
||||||
The bot recognizes _"Symbols"_. `$` for stocks and `$$` for cryptos. Example:
|
The bot recognizes _"Symbols"_. `$` for stocks and `$$` for cryptos. Example:
|
||||||
- `/chart $$eth` gets a month's Ethereum chart.
|
- `/chart $$eth` gets a month's Ethereum chart.
|
||||||
- `/dividend $psec` shows Prospect Capital's dividend info.
|
- `/dividend $psec` shows Prospect Capital's dividend info.
|
||||||
|
|
||||||
Mention a symbol, and the bot reveals its price.
|
Mention a symbol, and the bot reveals its price.
|
||||||
E.g., `What's $$btc's price since $tsla accepts it?` gives Bitcoin and Tesla prices.
|
E.g., `What's $$btc's price since $tsla accepts it?` gives Bitcoin and Tesla prices.
|
||||||
|
|
||||||
**Commands**
|
**Commands**
|
||||||
- `/donate [USD]`: Support the bot. 🎗️
|
- `/donate [USD]`: Support the bot. 🎗️
|
||||||
- `/intra $[symbol]`: Today's stock activity. 📈
|
- `/intra $[symbol]`: Today's stock activity. 📈
|
||||||
- `/chart $[symbol]`: Past month's stock chart. 📊
|
- `/chart $[symbol]`: Past month's stock chart. 📊
|
||||||
- `/trending`: What's hot in stocks and cryptos. 💬
|
- `/trending`: What's hot in stocks and cryptos. 💬
|
||||||
- `/help`: Bot assistance. 🆘
|
- `/help`: Bot assistance. 🆘
|
||||||
|
|
||||||
**Inline Features**
|
**Inline Features**
|
||||||
Search with @SimpleStockBot `[query]` anywhere.
|
Search with @SimpleStockBot `[query]` anywhere.
|
||||||
Pick a ticker, and the bot shares the current price in chat. Note: Prices can lag by an hour.
|
Pick a ticker, and the bot shares the current price in chat. Note: Prices can lag by an hour.
|
||||||
|
|
||||||
Data thanks to [marketdata.app](https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=telegram).
|
Data thanks to [marketdata.app](https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=telegram).
|
||||||
|
|
||||||
Bot issues? Use `/status` or [contact us](https://simplestockbot.com/contact).
|
Bot issues? Use `/status` or [contact us](https://simplestockbot.com/contact).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
donate_text = """
|
donate_text = """
|
||||||
Support Simple Stock Bot through [donations](https://www.buymeacoffee.com/Anson).
|
Support Simple Stock Bot through [donations](https://www.buymeacoffee.com/Anson).
|
||||||
All funds help maintain servers, with data from
|
All funds help maintain servers, with data from
|
||||||
[marketdata.app](https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=telegram).
|
[marketdata.app](https://dashboard.marketdata.app/marketdata/aff/go/misterbiggs?keyword=telegram).
|
||||||
|
|
||||||
**How to Donate?**
|
**How to Donate?**
|
||||||
1. Use `/donate [amount in USD]`. E.g., `/donate 2` donates 2 USD.
|
1. Use `/donate [amount in USD]`. E.g., `/donate 2` donates 2 USD.
|
||||||
2. Or, quickly donate at [buymeacoffee](https://www.buymeacoffee.com/Anson). No account needed, accepts Paypal & Credit card.
|
2. Or, quickly donate at [buymeacoffee](https://www.buymeacoffee.com/Anson). No account needed, accepts Paypal & Credit card.
|
||||||
|
|
||||||
For questions, visit our [website](https://simplestockbot.com).
|
For questions, visit our [website](https://simplestockbot.com).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# Not used by the bot but for updating commands with BotFather
|
# Not used by the bot but for updating commands with BotFather
|
||||||
commands = """
|
commands = """
|
||||||
donate - Donate to the bot 🎗️
|
donate - Donate to the bot 🎗️
|
||||||
help - Get some help using the bot. 🆘
|
help - Get some help using the bot. 🆘
|
||||||
trending - Trending Stocks and Cryptos. 💬
|
trending - Trending Stocks and Cryptos. 💬
|
||||||
intra - $[symbol] Plot since the last market open. 📈
|
intra - $[symbol] Plot since the last market open. 📈
|
||||||
chart - $[chart] Plot of the past month. 📊
|
chart - $[chart] Plot of the past month. 📊
|
||||||
"""
|
"""
|
||||||
|
1016
telegram/bot.py
1016
telegram/bot.py
File diff suppressed because it is too large
Load Diff
44
tests.py
44
tests.py
@ -1,23 +1,21 @@
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
import keyboard
|
import keyboard
|
||||||
|
|
||||||
tests = """$$xno
|
tests = """$$xno
|
||||||
$tsla
|
$tsla
|
||||||
/intra $tsla
|
/intra $tsla
|
||||||
/intra $$btc
|
/intra $$btc
|
||||||
/chart $tsla
|
/chart $tsla
|
||||||
/chart $$btc
|
/chart $$btc
|
||||||
/help
|
/help
|
||||||
/trending""".split(
|
/trending""".split("\n")
|
||||||
"\n"
|
|
||||||
)
|
print("press enter to start")
|
||||||
|
keyboard.wait("enter")
|
||||||
print("press enter to start")
|
|
||||||
keyboard.wait("enter")
|
for test in tests:
|
||||||
|
print(test)
|
||||||
for test in tests:
|
keyboard.write(test)
|
||||||
print(test)
|
time.sleep(1)
|
||||||
keyboard.write(test)
|
keyboard.press_and_release("enter")
|
||||||
time.sleep(1)
|
|
||||||
keyboard.press_and_release("enter")
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user