From 9d093e45b8e4be65a9eadb3a4dcb298eb44134e2 Mon Sep 17 00:00:00 2001 From: MuslemRahimi Date: Fri, 11 Apr 2025 17:12:37 +0200 Subject: [PATCH] add recent earnings to discord bot --- app/cron_discord_bot.py | 249 +++++++++++++++++++++++++++++++++++++++- app/primary_cron_job.py | 2 +- 2 files changed, 244 insertions(+), 7 deletions(-) diff --git a/app/cron_discord_bot.py b/app/cron_discord_bot.py index f38d468..8f530ff 100644 --- a/app/cron_discord_bot.py +++ b/app/cron_discord_bot.py @@ -1,11 +1,15 @@ import requests import time import orjson +import aiohttp +import aiofiles from datetime import datetime, timedelta, timezone, date import pytz import os from dotenv import load_dotenv - +import asyncio +import sqlite3 +import hashlib load_dotenv() @@ -18,7 +22,10 @@ N_minutes_ago = now - timedelta(minutes=30) DARK_POOL_WEBHOOK_URL = os.getenv("DISCORD_DARK_POOL_WEBHOOK") OPTIONS_FLOW_WEBHOOK_URL = os.getenv("DISCORD_OPTIONS_FLOW_WEBHOOK") +RECENT_EARNINGS_WEBHOOK_URL = os.getenv("DISCORD_RECENT_EARNINGS_WEBHOOK") +BENZINGA_API_KEY = os.getenv('BENZINGA_API_KEY') +headers = {"accept": "application/json"} def save_json(data): directory = "json/discord" @@ -26,7 +33,6 @@ def save_json(data): with open(directory+"/dark_pool.json", 'wb') as file: file.write(orjson.dumps(data)) - def format_number(num, decimal=False): """Abbreviate large numbers with B/M suffix and format appropriately""" # Handle scientific notation formats like 5E6 @@ -59,6 +65,130 @@ def format_number(num, decimal=False): else: return f"{num:,.0f}" # Format smaller numbers with commas +def abbreviate_number(n): + """ + Abbreviate a number to a readable format. + E.g., 1500 -> '1.5K', 2300000 -> '2.3M' + """ + if n is None: + return "N/A" + abs_n = abs(n) + if abs_n < 1000: + return str(n) + elif abs_n < 1_000_000: + return f"{n/1000:.1f}K" + elif abs_n < 1_000_000_000: + return f"{n/1_000_000:.1f}M" + else: + return f"{n/1_000_000_000:.1f}B" + +def remove_duplicates(elements): + seen = set() + unique_elements = [] + + for element in elements: + if element['symbol'] not in seen: + seen.add(element['symbol']) + unique_elements.append(element) + + return unique_elements + +def weekday(): + today = datetime.today() + if today.weekday() >= 5: # 5 = Saturday, 6 = Sunday + yesterday = today - timedelta(2) + else: + yesterday = today - timedelta(1) + + return yesterday.strftime('%Y-%m-%d') + + +async def get_recent_earnings(session): + today = datetime.today().strftime('%Y-%m-%d') + yesterday = weekday() + + url = "https://api.benzinga.com/api/v2.1/calendar/earnings" + res_list = [] + importance_list = ["1","2","3","4","5"] + + for importance in importance_list: + querystring = { + "token": BENZINGA_API_KEY, + "parameters[importance]": importance, + "parameters[date_from]": yesterday, + "parameters[date_to]": today, + "parameters[date_sort]": "date" + } + try: + async with session.get(url, params=querystring, headers=headers) as response: + res = orjson.loads(await response.text())['earnings'] + for item in res: + try: + symbol = item['ticker'] + name = item['name'] + time = item['time'] + date = item['date'] + updated = int(item['updated']) # Convert to integer for proper comparison + + # Convert numeric fields, handling empty strings + eps_prior = float(item['eps_prior']) if item['eps_prior'] != '' else None + eps_surprise = float(item['eps_surprise']) if item['eps_surprise'] != '' else None + eps = float(item['eps']) if item['eps'] != '' else 0 + revenue_prior = float(item['revenue_prior']) if item['revenue_prior'] != '' else None + revenue_surprise = float(item['revenue_surprise']) if item['revenue_surprise'] != '' else None + revenue = float(item['revenue']) if item['revenue'] != '' else None + + if (symbol in stock_symbols and + revenue is not None and + revenue_prior is not None and + eps_prior is not None and + eps is not None and + revenue_surprise is not None and + eps_surprise is not None): + + with open(f"json/quote/{symbol}.json","r") as file: + quote_data = orjson.loads(file.read()) + + market_cap = quote_data.get('marketCap',0) + price = quote_data.get('price',0) + changes_percentage = quote_data.get('changesPercentage',0) + + res_list.append({ + 'symbol': symbol, + 'name': name, + 'time': time, + 'date': date, + 'marketCap': market_cap, + 'epsPrior': eps_prior, + 'epsSurprise': eps_surprise, + 'eps': eps, + 'revenuePrior': revenue_prior, + 'revenueSurprise': revenue_surprise, + 'revenue': revenue, + 'price': price, + 'changesPercentage': changes_percentage, + 'updated': updated + }) + except Exception as e: + print('Recent Earnings:', e) + pass + except Exception as e: + print('API Request Error:', e) + pass + + # Remove duplicates + res_list = remove_duplicates(res_list) + + # Sort first by the most recent 'updated' timestamp, then by market cap + res_list.sort(key=lambda x: (-x['updated'], -x['marketCap'])) + + # Remove market cap before returning and limit to top 10 + res_list = [{k: v for k, v in d.items() if k not in ['updated']} for d in res_list] + + return res_list[:10] + + + def dark_pool_flow(): try: @@ -70,7 +200,7 @@ def dark_pool_flow(): with open(f"json/dark-pool/feed/data.json", "r") as file: res_list = orjson.loads(file.read()) - res_list = [item for item in res_list if float(item['premium']) >= 30E6 and datetime.fromisoformat(item['date']).date() == today] + res_list = [item for item in res_list if float(item['premium']) >= 100E6 and datetime.fromisoformat(item['date']).date() == today] if res_list: @@ -101,7 +231,7 @@ def dark_pool_flow(): message_timestamp = int((datetime.now() - timedelta(minutes=0)).timestamp()) embed = { - "color": 0xC475FD, # Green color from original embed + "color": 0xC475FD, "thumbnail": {"url": "https://stocknear.com/pwa-64x64.png"}, "title": "Dark Pool Order", "fields": [ @@ -256,7 +386,114 @@ def options_flow(): except Exception as e: print(f"Error sending message: {e}") +async def recent_earnings_message(): -if __name__ == "__main__": + try: + with open(f"json/discord/recent_earnings.json", "r") as file: + seen_list = orjson.loads(file.read()) + seen_list = [item for item in seen_list if datetime.fromisoformat(item['date']).date() == today] + except: + seen_list = [] + + async with aiohttp.ClientSession() as session: + res_list = await get_recent_earnings(session) + for item in res_list: + + unique_str = f"{item['date']}-{item['symbol']}-{item['revenue']}-{item['eps']}" + item['id'] = hashlib.md5(unique_str.encode()).hexdigest() + + + if res_list: + + if seen_list: + seen_ids = {item['id'] for item in seen_list} + else: + seen_ids = {} + + for item in res_list: + try: + if item != None and item['id'] not in seen_ids and item['marketCap'] > 100E9: + symbol = item['symbol'] + price = item['price'] + changes_percentage = round(item['changesPercentage'],2) + revenue = abbreviate_number(item['revenue']) + revenue_surprise = abbreviate_number(item.get("revenueSurprise", 0)) + eps = item['eps'] + eps_surprise = item.get("epsSurprise", 0) + revenue_surprise_text = "exceeds" if item["revenueSurprise"] > 0 else "misses" + eps_surprise_text = "exceeds" if eps_surprise > 0 else "misses" + + market_cap = abbreviate_number(item['marketCap']) + + revenue_yoy_change = (item["revenue"] / item["revenuePrior"] - 1) * 100 + revenue_yoy_direction = "decline" if (item["revenue"] / item["revenuePrior"] - 1) < 0 else "growth" + + eps_yoy_change = (item["eps"] / item["epsPrior"] - 1) * 100 + eps_yoy_direction = "decline" if (item["eps"] / item["epsPrior"] - 1) < 0 else "growth" + + message_timestamp = int((datetime.now() - timedelta(minutes=0)).timestamp()) + + embed = { + "color": 0xC475FD, + "thumbnail": {"url": "https://stocknear.com/pwa-64x64.png"}, + "title": "Earnings Surprise", + "fields": [ + {"name": "Symbol", "value": symbol, "inline": True}, + {"name": "", "value": "", "inline": True}, + {"name": "Market Cap", "value": market_cap, "inline": True}, + {"name": "Price", "value": str(price), "inline": True}, + {"name": "", "value": "", "inline": True}, + {"name": "% Change", "value": str(changes_percentage)+"%", "inline": True}, + {"name": "", "value": "", "inline": False}, + {"name": f"Revenue of {revenue} {revenue_surprise_text} estimates by {revenue_surprise}, with {revenue_yoy_change:.2f}% YoY {revenue_yoy_direction}.", "value": "", "inline": False}, + {"name": f"EPS of {eps} {eps_surprise_text} estimates by {eps_surprise}, with {eps_yoy_change:.2f}% YoY {eps_yoy_direction}.", "value": "", "inline": False}, + {"name": f"Data by Stocknear - ", "value": "", "inline": False}, + ], + "footer": {"text": ""} + } + + payload = { + "content": "", + "embeds": [embed] + } + + response = requests.post(RECENT_EARNINGS_WEBHOOK_URL, json=payload) + + if response.status_code in (200, 204): + seen_list.append({'date': item['date'], 'id': item['id'], 'symbol': symbol}) + print("Embed sent successfully!") + + else: + print(f"Failed to send embed. Status code: {response.status_code}") + print("Response content:", response.text) + + else: + print("Earnings already sent!") + + + except Exception as e: + print(e) + try: + with open("json/discord/recent_earnings.json","wb") as file: + file.write(orjson.dumps(seen_list)) + except: + pass + + +async def main(): options_flow() - dark_pool_flow() \ No newline at end of file + dark_pool_flow() + await recent_earnings_message() + +try: + con = sqlite3.connect('stocks.db') + cursor = con.cursor() + cursor.execute("PRAGMA journal_mode = wal") + cursor.execute("SELECT DISTINCT symbol FROM stocks") + stock_symbols = [row[0] for row in cursor.fetchall()] + con.close() + + asyncio.run(main()) + +except Exception as e: + print(e) \ No newline at end of file diff --git a/app/primary_cron_job.py b/app/primary_cron_job.py index c82b38b..e69be8b 100755 --- a/app/primary_cron_job.py +++ b/app/primary_cron_job.py @@ -427,7 +427,7 @@ schedule.every(5).minutes.do(run_threaded, run_push_notifications).tag('push_not schedule.every(5).minutes.do(run_threaded, run_market_flow).tag('market_flow_job') schedule.every(5).minutes.do(run_threaded, run_list).tag('stock_list_job') -schedule.every(5).minutes.do(run_threaded, run_discord_bot).tag('discord_bot') +schedule.every(2).minutes.do(run_threaded, run_discord_bot).tag('discord_bot')