Enhanced Email Format. Improved Unique Filling Retrieval
This commit is contained in:
@@ -6,6 +6,7 @@ from email.mime.text import MIMEText
|
|||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import os
|
import os
|
||||||
|
import uuid
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -27,20 +28,32 @@ query_api = QueryApi(api_key=SEC_API_KEY)
|
|||||||
# Point72 CIK
|
# Point72 CIK
|
||||||
CIK = '0001603466'
|
CIK = '0001603466'
|
||||||
|
|
||||||
# Query for latest two 13F-HR filings
|
# Query for latest 13F-HR filings
|
||||||
query = {
|
query = {
|
||||||
"query": f'cik:{CIK} formType:"13F-HR"',
|
"query": f'cik:{CIK} formType:"13F-HR"',
|
||||||
"sort": [{"filedAt": {"order": "desc"}}],
|
"sort": [{"filedAt": {"order": "desc"}}],
|
||||||
"size": 2
|
"size": 10 # Fetch more to ensure we get distinct periods
|
||||||
}
|
}
|
||||||
response = query_api.get_filings(query)
|
response = query_api.get_filings(query)
|
||||||
filings = response.get('filings', [])
|
filings = response.get('filings', [])
|
||||||
|
|
||||||
if len(filings) < 2:
|
# Filter unique periodOfReport and select the two most recent
|
||||||
raise Exception(f"Not enough filings found for comparison: {len(filings)} found")
|
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]
|
if len(unique_filings) < 2:
|
||||||
prev = filings[1]
|
raise Exception(f"Not enough unique filings found: {len(unique_filings)} found")
|
||||||
|
|
||||||
|
latest = unique_filings[0]
|
||||||
|
prev = unique_filings[1]
|
||||||
|
|
||||||
# Fetch holdings
|
# Fetch holdings
|
||||||
latest_holdings = latest.get('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:
|
if col not in latest_df.columns or col not in prev_df.columns:
|
||||||
raise KeyError(f"Column {col} missing in holdings data")
|
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
|
# Set index for comparison
|
||||||
key_col = 'cusip'
|
key_col = 'cusip'
|
||||||
latest_df = latest_df.set_index(key_col)
|
latest_df = latest_df.set_index(key_col)
|
||||||
@@ -83,28 +100,53 @@ removals = prev_df[~prev_df.index.isin(latest_df.index)]
|
|||||||
# Changes >10%
|
# Changes >10%
|
||||||
both = latest_df.index.intersection(prev_df.index)
|
both = latest_df.index.intersection(prev_df.index)
|
||||||
changes = latest_df.loc[both].join(prev_df.loc[both], lsuffix='_new', rsuffix='_old')
|
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_new'] = pd.to_numeric(changes['shrsOrPrnAmt_new'], errors='coerce')
|
||||||
changes['shrsOrPrnAmt_old'] = pd.to_numeric(changes['shrsOrPrnAmt_old'], 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['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 = 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 = """
|
||||||
|
<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"""
|
summary = f"""
|
||||||
Point72 13F Changes {prev['periodOfReport']} to {latest['periodOfReport']}
|
<html>
|
||||||
|
<head>{html_style}</head>
|
||||||
Additions ({len(additions)}):
|
<body>
|
||||||
{additions[['nameOfIssuer', 'shrsOrPrnAmt', 'value']].to_string() if not additions.empty else 'None'}
|
<h2>Point72 13F Changes {prev['periodOfReport']} to {latest['periodOfReport']}</h2>
|
||||||
|
{df_to_html(additions, ['nameOfIssuer', 'shrsOrPrnAmt', 'value_formatted'], 'Additions')}
|
||||||
Removals ({len(removals)}):
|
{df_to_html(removals, ['nameOfIssuer', 'shrsOrPrnAmt', 'value_formatted'], 'Removals')}
|
||||||
{removals[['nameOfIssuer', 'shrsOrPrnAmt', 'value']].to_string() if not removals.empty else 'None'}
|
{df_to_html(changes, ['nameOfIssuer_new', 'share_change'], 'Changes')}
|
||||||
|
</body>
|
||||||
Changes ({len(changes)}):
|
</html>
|
||||||
{changes[['nameOfIssuer_new', 'share_change']].to_string() if not changes.empty else 'None'}
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Email
|
# Email
|
||||||
@@ -112,7 +154,7 @@ msg = MIMEMultipart()
|
|||||||
msg['From'] = EMAIL_SENDER
|
msg['From'] = EMAIL_SENDER
|
||||||
msg['To'] = EMAIL_RECIPIENT
|
msg['To'] = EMAIL_RECIPIENT
|
||||||
msg['Subject'] = f'Point72 13F Update {latest["periodOfReport"]}'
|
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 = SMTP(SMTP_SERVER, SMTP_PORT)
|
||||||
server.starttls()
|
server.starttls()
|
||||||
|
|||||||
Reference in New Issue
Block a user