# 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 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") # 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}") # HTML table styling html_style = """ """ # Convert DataFrames to HTML tables def df_to_html(df, columns, title): if df.empty: return f"

{title} (0)

None

" df_subset = df[columns].reset_index(drop=True) return f"

{title} ({len(df)})

{df_subset.to_html(index=False, border=0, classes='table')}" # Summary as HTML summary = f""" {html_style}

Point72 13F Changes {prev['periodOfReport']} to {latest['periodOfReport']}

{df_to_html(additions, ['nameOfIssuer', 'shrsOrPrnAmt', 'value_formatted'], 'Additions')} {df_to_html(removals, ['nameOfIssuer', 'shrsOrPrnAmt', 'value_formatted'], 'Removals')} {df_to_html(changes, ['nameOfIssuer_new', 'share_change'], 'Changes')} """ # 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!")