Files
HedgeAnalyzer/HedgeAnalyzer.py
2025-09-21 09:01:11 -04:00

179 lines
5.5 KiB
Python
Executable File

# pip install sec-api pandas python-dotenv
from sec_api import QueryApi
import pandas as pd
from smtplib import SMTP
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from dotenv import load_dotenv
import os
import uuid
# Load environment variables
load_dotenv()
# Configuration from .env
EMAIL_SENDER = os.getenv('EMAIL_SENDER')
EMAIL_RECIPIENT = os.getenv('EMAIL_RECIPIENT')
EMAIL_PASSWORD = os.getenv('EMAIL_PASSWORD')
SMTP_SERVER = os.getenv('SMTP_SERVER', 'smtp.gmail.com')
SMTP_PORT = int(os.getenv('SMTP_PORT', 587))
SEC_API_KEY = os.getenv('SEC_API_KEY')
if not SEC_API_KEY:
raise ValueError("SEC_API_KEY not set in .env file")
# Initialize SEC API
query_api = QueryApi(api_key=SEC_API_KEY)
# Point72 CIK
CIK = '0001603466'
# Query for latest 13F-HR filings
query = {
"query": f'cik:{CIK} formType:"13F-HR"',
"sort": [{"filedAt": {"order": "desc"}}],
"size": 10 # Fetch more to ensure we get distinct periods
}
response = query_api.get_filings(query)
filings = response.get('filings', [])
# Filter unique periodOfReport and select the two most recent
filings = sorted(filings, key=lambda x: x['periodOfReport'], reverse=True)
unique_filings = []
seen_periods = set()
for filing in filings:
period = filing['periodOfReport']
if period not in seen_periods:
unique_filings.append(filing)
seen_periods.add(period)
if len(unique_filings) == 2:
break
if len(unique_filings) < 2:
raise Exception(f"Not enough unique filings found: {len(unique_filings)} found")
latest = unique_filings[0]
prev = unique_filings[1]
# Fetch holdings
latest_holdings = latest.get('holdings', [])
prev_holdings = prev.get('holdings', [])
if not latest_holdings or not prev_holdings:
raise Exception("No holdings data found in filings")
# Convert holdings to DataFrame
latest_df = pd.DataFrame(latest_holdings)
prev_df = pd.DataFrame(prev_holdings)
# Extract share amount from shrsOrPrnAmt dictionary
def extract_shares(row):
if isinstance(row, dict):
return row.get('sshPrnamt', 0)
return row
if 'shrsOrPrnAmt' in latest_df.columns:
latest_df['shrsOrPrnAmt'] = latest_df['shrsOrPrnAmt'].apply(extract_shares)
prev_df['shrsOrPrnAmt'] = prev_df['shrsOrPrnAmt'].apply(extract_shares)
# Verify required columns (ticker is optional but we'll add it if present)
required_cols = ['cusip', 'nameOfIssuer', 'shrsOrPrnAmt', 'value']
for col in required_cols:
if col not in latest_df.columns or col not in prev_df.columns:
raise KeyError(f"Column {col} missing in holdings data")
# Add ticker column if present
if 'ticker' in latest_df.columns:
latest_df['ticker'] = latest_df['ticker'].fillna('N/A')
prev_df['ticker'] = prev_df['ticker'].fillna('N/A')
else:
latest_df['ticker'] = 'N/A'
prev_df['ticker'] = 'N/A'
# Format value with commas and dollar sign
latest_df['value_formatted'] = latest_df['value'].apply(lambda x: f"${x:,.0f}")
prev_df['value_formatted'] = prev_df['value'].apply(lambda x: f"${x:,.0f}")
# Set index for comparison
key_col = 'cusip'
latest_df = latest_df.set_index(key_col)
prev_df = prev_df.set_index(key_col)
# Additions
additions = latest_df[~latest_df.index.isin(prev_df.index)]
# Removals
removals = prev_df[~prev_df.index.isin(latest_df.index)]
# Changes >10%
both = latest_df.index.intersection(prev_df.index)
changes = latest_df.loc[both].join(prev_df.loc[both], lsuffix='_new', rsuffix='_old')
changes['shrsOrPrnAmt_new'] = pd.to_numeric(changes['shrsOrPrnAmt_new'], errors='coerce')
changes['shrsOrPrnAmt_old'] = pd.to_numeric(changes['shrsOrPrnAmt_old'], errors='coerce')
changes['share_change'] = changes['shrsOrPrnAmt_new'] - changes['shrsOrPrnAmt_old']
changes = changes[changes['share_change'].notna()]
changes = changes[abs(changes['share_change']) / changes['shrsOrPrnAmt_old'].replace(0, 1) > 0.1]
changes['share_change'] = changes['share_change'].apply(lambda x: f"{x:,.0f}")
# Add ticker_new for changes
if 'ticker' in changes.columns:
changes['ticker_new'] = changes['ticker_new'].fillna('N/A')
else:
changes['ticker_new'] = 'N/A'
# HTML table styling
html_style = """
<style>
table {
border-collapse: collapse;
width: 100%;
font-family: Arial, sans-serif;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
font-weight: bold;
}
tr:nth-child(even) {background-color: #f9f9f9;}
tr:hover {background-color: #f5f5f5;}
</style>
"""
# Convert DataFrames to HTML tables
def df_to_html(df, columns, title):
if df.empty:
return f"<h3>{title} (0)</h3><p>None</p>"
df_subset = df[columns].reset_index(drop=True)
return f"<h3>{title} ({len(df)})</h3>{df_subset.to_html(index=False, border=0, classes='table')}"
# Summary as HTML
summary = f"""
<html>
<head>{html_style}</head>
<body>
<h2>Point72 13F Changes {prev['periodOfReport']} to {latest['periodOfReport']}</h2>
{df_to_html(additions, ['nameOfIssuer', 'ticker', 'shrsOrPrnAmt', 'value_formatted'], 'Additions')}
{df_to_html(removals, ['nameOfIssuer', 'ticker', 'shrsOrPrnAmt', 'value_formatted'], 'Removals')}
{df_to_html(changes, ['nameOfIssuer_new', 'ticker_new', 'share_change'], 'Changes')}
</body>
</html>
"""
# Email
msg = MIMEMultipart()
msg['From'] = EMAIL_SENDER
msg['To'] = EMAIL_RECIPIENT
msg['Subject'] = f'Point72 13F Update {latest["periodOfReport"]}'
msg.attach(MIMEText(summary, 'html'))
server = SMTP(SMTP_SERVER, SMTP_PORT)
server.starttls()
server.login(EMAIL_SENDER, EMAIL_PASSWORD)
server.send_message(msg)
server.quit()
print("Email sent!")