diff --git a/HedgeAnalyzer.py b/HedgeAnalyzer.py index 86e4f6a..b7b729e 100755 --- a/HedgeAnalyzer.py +++ b/HedgeAnalyzer.py @@ -6,6 +6,7 @@ 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() @@ -27,20 +28,32 @@ query_api = QueryApi(api_key=SEC_API_KEY) # Point72 CIK CIK = '0001603466' -# Query for latest two 13F-HR filings +# Query for latest 13F-HR filings query = { "query": f'cik:{CIK} formType:"13F-HR"', "sort": [{"filedAt": {"order": "desc"}}], - "size": 2 + "size": 10 # Fetch more to ensure we get distinct periods } response = query_api.get_filings(query) filings = response.get('filings', []) -if len(filings) < 2: - raise Exception(f"Not enough filings found for comparison: {len(filings)} found") +# 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 -latest = filings[0] -prev = filings[1] +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', []) @@ -69,6 +82,10 @@ 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) @@ -83,28 +100,53 @@ 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') - -# Ensure share columns are numeric changes['shrsOrPrnAmt_new'] = pd.to_numeric(changes['shrsOrPrnAmt_new'], errors='coerce') changes['shrsOrPrnAmt_old'] = pd.to_numeric(changes['shrsOrPrnAmt_old'], errors='coerce') - -# Calculate share change changes['share_change'] = changes['shrsOrPrnAmt_new'] - changes['shrsOrPrnAmt_old'] -changes = changes[changes['share_change'].notna()] # Remove rows with NaN changes +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}") -# Summary +# 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""" -Point72 13F Changes {prev['periodOfReport']} to {latest['periodOfReport']} - -Additions ({len(additions)}): -{additions[['nameOfIssuer', 'shrsOrPrnAmt', 'value']].to_string() if not additions.empty else 'None'} - -Removals ({len(removals)}): -{removals[['nameOfIssuer', 'shrsOrPrnAmt', 'value']].to_string() if not removals.empty else 'None'} - -Changes ({len(changes)}): -{changes[['nameOfIssuer_new', 'share_change']].to_string() if not changes.empty else 'None'} + +{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 @@ -112,7 +154,7 @@ msg = MIMEMultipart() msg['From'] = EMAIL_SENDER msg['To'] = EMAIL_RECIPIENT msg['Subject'] = f'Point72 13F Update {latest["periodOfReport"]}' -msg.attach(MIMEText(summary, 'plain')) +msg.attach(MIMEText(summary, 'html')) server = SMTP(SMTP_SERVER, SMTP_PORT) server.starttls()