476 lines
18 KiB
Python
476 lines
18 KiB
Python
import praw
|
||
import orjson
|
||
from datetime import datetime
|
||
import os
|
||
from dotenv import load_dotenv
|
||
import time
|
||
import ujson
|
||
import argparse
|
||
|
||
|
||
def parse_args():
|
||
parser = argparse.ArgumentParser(description='Market Status')
|
||
parser.add_argument('--market_status', choices=[0, 1, 2], type=int, default=0,
|
||
help='Market status: 0 for Open (default), 1 for Premarket, 2 for Afterhours')
|
||
return parser.parse_args()
|
||
|
||
def get_current_weekday():
|
||
"""Return the current weekday name."""
|
||
return datetime.now().strftime("%A")
|
||
|
||
def format_time(time_str):
|
||
"""Format time string to AM/PM format"""
|
||
if not time_str:
|
||
return ""
|
||
|
||
try:
|
||
time_parts = time_str.split(':')
|
||
hours = int(time_parts[0])
|
||
minutes = int(time_parts[1])
|
||
|
||
period = "AM" if hours < 12 else "PM"
|
||
if hours > 12:
|
||
hours -= 12
|
||
elif hours == 0:
|
||
hours = 12
|
||
|
||
return f"{hours:02d}:{minutes:02d} {period}"
|
||
except:
|
||
return ""
|
||
|
||
|
||
def format_number(num):
|
||
"""Abbreviate large numbers with B/M suffix"""
|
||
if num >= 1_000_000_000:
|
||
return f"{num / 1_000_000_000:.2f}B"
|
||
elif num >= 1_000_000:
|
||
return f"{num / 1_000_000:.2f}M"
|
||
return f"{num:,.0f}"
|
||
|
||
def calculate_yoy_change(current, prior):
|
||
"""Calculate year-over-year percentage change"""
|
||
if prior and prior != 0:
|
||
return ((current / prior - 1) * 100)
|
||
return 0
|
||
|
||
def get_market_timing(time_str):
|
||
"""Determine if earnings are before, after, or during market hours"""
|
||
if not time_str:
|
||
return ""
|
||
|
||
try:
|
||
time_parts = time_str.split(':')
|
||
hours = int(time_parts[0])
|
||
minutes = int(time_parts[1])
|
||
|
||
if hours < 9 or (hours == 9 and minutes <= 30):
|
||
return "before market opens."
|
||
elif hours >= 16:
|
||
return "after market closes."
|
||
else:
|
||
return "during market."
|
||
except:
|
||
return ""
|
||
|
||
def format_upcoming_earnings_data(earnings_data):
|
||
"""Format earnings data into Reddit-friendly markdown with hyperlinks."""
|
||
formatted_items = []
|
||
|
||
for item in earnings_data:
|
||
symbol = item.get('symbol', None)
|
||
if symbol is not None:
|
||
name = item.get('name', 'Unknown')
|
||
market_timing = get_market_timing(item.get('time'))
|
||
revenue_formatted = format_number(item.get('revenueEst', 0))
|
||
revenue_yoy = calculate_yoy_change(item.get('revenueEst', 0), item.get('revenuePrior', 1)) # Avoid division by zero
|
||
eps_yoy = calculate_yoy_change(item.get('epsEst', 0), item.get('epsPrior', 1)) # Avoid division by zero
|
||
|
||
# Determine reporting time text
|
||
if item.get('isToday'):
|
||
report_timing = "will report today"
|
||
else:
|
||
current_day = datetime.now().strftime('%A')
|
||
report_timing = "will report tomorrow" if current_day in ['Monday', 'Tuesday', 'Wednesday', 'Thursday'] else "will report Monday"
|
||
|
||
# Create hyperlink for symbol
|
||
symbol_link = f"[{symbol}](https://stocknear.com/stocks/{symbol})"
|
||
|
||
# Format the entry text
|
||
entry = (
|
||
f"* **{name}** ({symbol_link}) {report_timing} {market_timing} "
|
||
f"Analysts estimate {revenue_formatted} in revenue ({revenue_yoy:.2f}% YoY) and "
|
||
f"${item.get('epsEst', 0):.2f} in earnings per share ({eps_yoy:.2f}% YoY).\n\n"
|
||
)
|
||
formatted_items.append(entry)
|
||
|
||
return "".join(formatted_items)
|
||
|
||
def format_recent_earnings_data(earnings_data):
|
||
"""Format earnings data into Reddit-friendly markdown with bullet points."""
|
||
formatted_items = []
|
||
|
||
for item in earnings_data:
|
||
symbol = item.get('symbol', None)
|
||
if symbol is not None:
|
||
name = item.get('name', 'Unknown')
|
||
time = format_time(item.get('time', ''))
|
||
|
||
# Financial calculations
|
||
revenue = item.get('revenue', 0) # Changed from revenueEst to revenue for actual results
|
||
revenue_prior = item.get('revenuePrior', 1)
|
||
revenue_surprise = item.get('revenueSurprise', 0)
|
||
eps = item.get('eps', 0) # Changed from epsEst to eps for actual results
|
||
eps_prior = item.get('epsPrior', 1)
|
||
eps_surprise = item.get('epsSurprise', 0)
|
||
|
||
# Calculate YoY changes
|
||
revenue_yoy = calculate_yoy_change(revenue, revenue_prior)
|
||
eps_yoy = calculate_yoy_change(eps, eps_prior)
|
||
|
||
# Format numbers
|
||
revenue_formatted = format_number(revenue)
|
||
revenue_surprise_formatted = format_number(abs(revenue_surprise))
|
||
|
||
# Determine growth/decline text
|
||
revenue_trend = "growth" if revenue_yoy >= 0 else "decline"
|
||
eps_trend = "growth" if eps_yoy >= 0 else "decline"
|
||
|
||
# Create hyperlink for symbol
|
||
symbol_link = f"[{symbol}](https://stocknear.com/stocks/{symbol})"
|
||
|
||
# Format the entry text with nested bullet points
|
||
entry = (
|
||
f"**{name}** ({symbol_link}) has released its quarterly earnings at {time}:\n\n"
|
||
f"* Revenue of {revenue_formatted} "
|
||
f"{'exceeds' if revenue_surprise > 0 else 'misses'} estimates by {revenue_surprise_formatted}, "
|
||
f"with {revenue_yoy:.2f}% YoY {revenue_trend}.\n\n"
|
||
f"* EPS of ${eps:.2f} "
|
||
f"{'exceeds' if eps_surprise > 0 else 'misses'} estimates by ${abs(eps_surprise):.2f}, "
|
||
f"with {eps_yoy:.2f}% YoY {eps_trend}.\n\n"
|
||
)
|
||
|
||
formatted_items.append(entry)
|
||
|
||
return "".join(formatted_items)
|
||
|
||
def format_afterhour_market():
|
||
try:
|
||
# Load gainers data
|
||
with open("json/market-movers/afterhours/gainers.json", 'r') as file:
|
||
data = ujson.load(file)
|
||
gainers = [
|
||
{'symbol': item['symbol'], 'name': item['name'], 'price': item['price'],
|
||
'changesPercentage': item['changesPercentage'], 'marketCap': item['marketCap']}
|
||
for item in data[:5]
|
||
]
|
||
|
||
# Load losers data
|
||
with open("json/market-movers/afterhours/losers.json", 'r') as file:
|
||
data = ujson.load(file)
|
||
losers = [
|
||
{'symbol': item['symbol'], 'name': item['name'], 'price': item['price'],
|
||
'changesPercentage': item['changesPercentage'], 'marketCap': item['marketCap']}
|
||
for item in data[:5]
|
||
]
|
||
|
||
market_movers = {'gainers': gainers, 'losers': losers}
|
||
|
||
except Exception as e:
|
||
print(f"Error loading market data: {e}")
|
||
market_movers = {'gainers': [], 'losers': []}
|
||
|
||
# Create Gainers Table
|
||
gainers_table = "| Symbol | Name | Price | Change (%) | Market Cap |\n"
|
||
gainers_table += "|:------:|:-----|------:|-----------:|-----------:|\n"
|
||
for gainer in market_movers["gainers"]:
|
||
gainers_table += (
|
||
f"| [{gainer['symbol']}](https://stocknear.com/stocks/{gainer['symbol']}) | {gainer['name'][:30]} | "
|
||
f"{gainer['price']:.2f} | +{gainer['changesPercentage']:.2f}% | "
|
||
f"{format_number(gainer['marketCap'])} |\n"
|
||
)
|
||
|
||
# Create Losers Table
|
||
losers_table = "| Symbol | Name | Price | Change (%) | Market Cap |\n"
|
||
losers_table += "|:------:|:-----|------:|-----------:|-----------:|\n"
|
||
for loser in market_movers["losers"]:
|
||
losers_table += (
|
||
f"| [{loser['symbol']}](https://stocknear.com/stocks/{loser['symbol']}) | {loser['name'][:30]} | "
|
||
f"{loser['price']:.2f} | {loser['changesPercentage']:.2f}% | "
|
||
f"{format_number(loser['marketCap'])} |\n"
|
||
)
|
||
# Construct final markdown text
|
||
return f"""
|
||
|
||
Here's a summary of today's After-Hours Gainers and Losers, showcasing stocks that stood out after the market closed.
|
||
|
||
### 📈 After-Hours Gainers
|
||
|
||
{gainers_table}
|
||
|
||
### 📉 After-Hours Losers
|
||
|
||
{losers_table}
|
||
|
||
More info can be found here: [After-Hours Gainers and Losers](https://stocknear.com/market-mover/afterhours/gainers)
|
||
"""
|
||
|
||
def format_premarket_market():
|
||
try:
|
||
# Load gainers data
|
||
with open("json/market-movers/premarket/gainers.json", 'r') as file:
|
||
data = ujson.load(file)
|
||
gainers = [
|
||
{'symbol': item['symbol'], 'name': item['name'], 'price': item['price'],
|
||
'changesPercentage': item['changesPercentage'], 'marketCap': item['marketCap']}
|
||
for item in data[:5]
|
||
]
|
||
|
||
# Load losers data
|
||
with open("json/market-movers/premarket/losers.json", 'r') as file:
|
||
data = ujson.load(file)
|
||
losers = [
|
||
{'symbol': item['symbol'], 'name': item['name'], 'price': item['price'],
|
||
'changesPercentage': item['changesPercentage'], 'marketCap': item['marketCap']}
|
||
for item in data[:5]
|
||
]
|
||
|
||
market_movers = {'gainers': gainers, 'losers': losers}
|
||
|
||
except Exception as e:
|
||
print(f"Error loading market data: {e}")
|
||
market_movers = {'gainers': [], 'losers': []}
|
||
|
||
# Create Gainers Table
|
||
gainers_table = "| Symbol | Name | Price | Change (%) | Market Cap |\n"
|
||
gainers_table += "|:------:|:-----|------:|-----------:|-----------:|\n"
|
||
for gainer in market_movers["gainers"]:
|
||
gainers_table += (
|
||
f"| [{gainer['symbol']}](https://stocknear.com/stocks/{gainer['symbol']}) | {gainer['name'][:30]} | "
|
||
f"{gainer['price']:.2f} | +{gainer['changesPercentage']:.2f}% | "
|
||
f"{format_number(gainer['marketCap'])} |\n"
|
||
)
|
||
|
||
# Create Losers Table
|
||
losers_table = "| Symbol | Name | Price | Change (%) | Market Cap |\n"
|
||
losers_table += "|:------:|:-----|------:|-----------:|-----------:|\n"
|
||
for loser in market_movers["losers"]:
|
||
losers_table += (
|
||
f"| [{loser['symbol']}](https://stocknear.com/stocks/{loser['symbol']}) | {loser['name'][:30]} | "
|
||
f"{loser['price']:.2f} | {loser['changesPercentage']:.2f}% | "
|
||
f"{format_number(loser['marketCap'])} |\n"
|
||
)
|
||
# Construct final markdown text
|
||
return f"""
|
||
|
||
Here's a summary of today's Premarket Gainers and Losers, showcasing stocks that stood out before the market opened.
|
||
|
||
### 📈 Premarket Gainers
|
||
|
||
{gainers_table}
|
||
|
||
### 📉 Premarket Losers
|
||
|
||
{losers_table}
|
||
|
||
More info can be found here: [Premarket Gainers and Losers](https://stocknear.com/market-mover/premarket/gainers)
|
||
"""
|
||
|
||
def create_post(data_type, info_text):
|
||
include_rsi = False
|
||
try:
|
||
# Use the parameter passed to the function
|
||
with open(f"json/stocks-list/list/{data_type}.json", 'r') as file:
|
||
data = ujson.load(file)
|
||
|
||
# Limit to first 5 items and select specific fields
|
||
include_rsi = data_type in ["overbought-stocks", "oversold-stocks"]
|
||
data = [
|
||
{
|
||
'rank': item['rank'],
|
||
'symbol': item['symbol'],
|
||
'price': item['price'],
|
||
'changesPercentage': item['changesPercentage'],
|
||
'marketCap': item['marketCap'],
|
||
'rsi': item.get('rsi') if include_rsi else None
|
||
}
|
||
for item in data[:5]
|
||
]
|
||
|
||
except Exception as e:
|
||
print(f"Error loading data: {e}")
|
||
data = []
|
||
|
||
# Create Markdown table headers
|
||
if include_rsi:
|
||
data_table = "| Rank | Symbol | RSI | Price | Change (%) | Market Cap |\n"
|
||
data_table += "|:----:|:------|----:|------:|-----------:|-----------:|\n"
|
||
else:
|
||
data_table = "| Rank | Symbol | Price | Change (%) | Market Cap |\n"
|
||
data_table += "|:----:|:------|------:|-----------:|-----------:|\n"
|
||
|
||
# Generate table rows
|
||
for item in data:
|
||
if include_rsi:
|
||
data_table += (
|
||
f"| {item['rank']} | [{item['symbol']}](https://stocknear.com/stocks/{item['symbol']}) | "
|
||
f"{item['rsi']:.2f} | {item['price']:.2f} | {'+' if item['changesPercentage'] > 0 else ''}{item['changesPercentage']:.2f}% | "
|
||
f"{format_number(item['marketCap'])} |\n"
|
||
)
|
||
else:
|
||
data_table += (
|
||
f"| {item['rank']} | [{item['symbol']}](https://stocknear.com/stocks/{item['symbol']}) | "
|
||
f"{item['price']:.2f} | {'+' if item['changesPercentage'] > 0 else ''}{item['changesPercentage']:.2f}% | "
|
||
f"{format_number(item['marketCap'])} |\n"
|
||
)
|
||
|
||
# Return the Markdown string
|
||
return f"""
|
||
|
||
|
||
{data_table}
|
||
|
||
The complete list can be found [here](https://stocknear.com/list/penny-stocks)
|
||
|
||
*{info_text}*
|
||
|
||
*PS: If you find this post valuable please leave an upvote. Would love to hear what you guys think.*
|
||
"""
|
||
|
||
|
||
|
||
|
||
def post_to_reddit():
|
||
# Load environment variables
|
||
load_dotenv()
|
||
args = parse_args()
|
||
market_status = args.market_status
|
||
|
||
|
||
# Initialize Reddit instance
|
||
reddit = praw.Reddit(
|
||
client_id=os.getenv('REDDIT_BOT_API_KEY'),
|
||
client_secret=os.getenv('REDDIT_BOT_API_SECRET'),
|
||
username=os.getenv('REDDIT_USERNAME'),
|
||
password=os.getenv('REDDIT_PASSWORD'),
|
||
user_agent=os.getenv('REDDIT_USER_AGENT', 'script:my_bot:v1.0 (by /u/username)')
|
||
)
|
||
|
||
# Define the subreddit
|
||
subreddit = reddit.subreddit("stocknear")
|
||
|
||
flair_choices = subreddit.flair.link_templates # Get submission flair templates
|
||
|
||
# Print all submission flairs
|
||
'''
|
||
print("Submission Flairs:")
|
||
for flair in flair_choices:
|
||
print(f"ID: {flair['id']} | Text: {flair['text']} | CSS Class: {flair['css_class']} | Mod Only: {flair['mod_only']}")
|
||
'''
|
||
|
||
|
||
# Get current date with formatting
|
||
today = datetime.now()
|
||
month_str = today.strftime("%b")
|
||
day = today.day
|
||
year = today.year
|
||
day_suffix = "th" if 11 <= day <= 13 else {1: "st", 2: "nd", 3: "rd"}.get(day % 10, "th")
|
||
formatted_date = f"{month_str} {day}{day_suffix} {year}"
|
||
|
||
# Load and parse data from JSON file
|
||
with open("json/dashboard/data.json", "rb") as file:
|
||
data = orjson.loads(file.read())
|
||
|
||
|
||
post_configs = [
|
||
{
|
||
"data_type": "penny-stocks",
|
||
"title": f"Top 5 Actively Traded Penny Stocks by Volume 🚀",
|
||
"info_text": "Penny stocks are generally defined as stocks trading below $5 per share. This list is filtered to show only stocks with a volume over 10K.",
|
||
"flair_id": "b348676c-e451-11ee-8572-328509439585"
|
||
},
|
||
{
|
||
"data_type": "overbought-stocks",
|
||
"title": f"Top 5 Most Overbought Companies 📉",
|
||
'info_text': "I’ve compiled a list of the top 5 most overbought companies based on RSI (Relative Strength Index) data. For those who don’t know, RSI is a popular indicator that ranges from 0 to 100, with values above 70 typically indicating that a stock is overbought.",
|
||
"flair_id": "b348676c-e451-11ee-8572-328509439585"
|
||
},
|
||
{
|
||
"data_type": "oversold-stocks",
|
||
"title": f"Top 5 Most Oversold Companies 📈",
|
||
'info_text': "I’ve compiled a list of the top 5 most oversold companies based on RSI (Relative Strength Index) data. For those who don’t know, RSI is a popular indicator that ranges from 0 to 100, with values below 30 typically indicating that a stock is oversold.",
|
||
"flair_id": "b348676c-e451-11ee-8572-328509439585"
|
||
},
|
||
]
|
||
|
||
for post in post_configs:
|
||
formatted_text = create_post(post['data_type'], post['info_text'])
|
||
title = config["title"]
|
||
flair_id = config["flair_id"]
|
||
|
||
# Submit the post
|
||
post = subreddit.submit(
|
||
title=title,
|
||
selftext=formatted_text,
|
||
flair_id=flair_id
|
||
)
|
||
|
||
'''
|
||
# Define the post configurations
|
||
post_configs = [
|
||
{
|
||
"data_key": "upcomingEarnings",
|
||
"format_func": format_upcoming_earnings_data,
|
||
"title": f"Upcoming Earnings for {formatted_date}",
|
||
"flair_id": "b9f76638-772e-11ef-96c1-0afbf26bd890"
|
||
},
|
||
{
|
||
"data_key": "recentEarnings",
|
||
"format_func": format_recent_earnings_data,
|
||
"title": f"Recent Earnings for {formatted_date}",
|
||
"flair_id": "b9f76638-772e-11ef-96c1-0afbf26bd890"
|
||
},
|
||
]
|
||
|
||
if market_status == 0:
|
||
try:
|
||
# Loop through post configurations to submit each post
|
||
for config in post_configs:
|
||
formatted_text = config["format_func"](data.get(config["data_key"], []))
|
||
title = config["title"]
|
||
flair_id = config["flair_id"]
|
||
|
||
# Submit the post
|
||
post = subreddit.submit(
|
||
title=title,
|
||
selftext=formatted_text,
|
||
flair_id=flair_id
|
||
)
|
||
print(f"Post created successfully: {post.url}")
|
||
|
||
except praw.exceptions.PRAWException as e:
|
||
print(f"Error posting to Reddit: {str(e)}")
|
||
except Exception as e:
|
||
print(f"Unexpected error: {str(e)}")
|
||
|
||
elif market_status == 1: #premarket
|
||
try:
|
||
formatted_content = format_premarket_market()
|
||
title = "Premarket Gainers and Losers for Today 🚀📉"
|
||
post = subreddit.submit(title, selftext=formatted_content, flair_id="b348676c-e451-11ee-8572-328509439585")
|
||
print(f"Post created successfully")
|
||
except Exception as e:
|
||
print(f"Error posting to Reddit: {str(e)}")
|
||
|
||
elif market_status == 2: #aftermarket
|
||
try:
|
||
formatted_content = format_afterhour_market()
|
||
title = "Afterhours Gainers and Losers for Today 🚀📉"
|
||
post = subreddit.submit(title, selftext=formatted_content, flair_id="b348676c-e451-11ee-8572-328509439585")
|
||
print(f"Post created successfully")
|
||
except Exception as e:
|
||
print(f"Error posting to Reddit: {str(e)}")
|
||
'''
|
||
|
||
if __name__ == "__main__":
|
||
post_to_reddit()
|