189 lines
6.5 KiB
Python
189 lines
6.5 KiB
Python
"""
|
|
Alert Service
|
|
Handles alert stream matching for options flow
|
|
"""
|
|
import pandas as pd
|
|
import asyncpg
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, List, Optional
|
|
import pytz
|
|
import re
|
|
|
|
|
|
class AlertService:
|
|
"""Service for matching alerts with flow data"""
|
|
|
|
def __init__(self, pool: asyncpg.Pool):
|
|
self.pool = pool
|
|
self.ct_tz = pytz.timezone('America/Chicago')
|
|
self.utc_tz = pytz.UTC
|
|
|
|
def parse_alert_timestamp(self, date_str: str, time_str: str) -> Optional[datetime]:
|
|
"""Parse alert timestamp from date and time strings"""
|
|
if pd.isna(date_str) or pd.isna(time_str):
|
|
return None
|
|
|
|
date_str = str(date_str).strip()
|
|
time_str = str(time_str).strip()
|
|
|
|
# Try YYYY-MM-DD format
|
|
if re.match(r'^\d{4}-\d{2}-\d{2}$', date_str):
|
|
try:
|
|
# Try with seconds
|
|
full_str = f"{date_str} {time_str}"
|
|
dt = datetime.strptime(full_str, '%Y-%m-%d %I:%M:%S %p')
|
|
return self.ct_tz.localize(dt).astimezone(self.utc_tz)
|
|
except ValueError:
|
|
try:
|
|
# Try without seconds
|
|
full_str = f"{date_str} {time_str}"
|
|
dt = datetime.strptime(full_str, '%Y-%m-%d %I:%M %p')
|
|
return self.ct_tz.localize(dt).astimezone(self.utc_tz)
|
|
except ValueError:
|
|
pass
|
|
|
|
# Try MM/DD/YYYY format
|
|
if re.match(r'^\d{1,2}/\d{1,2}/\d{2,4}$', date_str):
|
|
try:
|
|
full_str = f"{date_str} {time_str}"
|
|
dt = datetime.strptime(full_str, '%m/%d/%Y %I:%M:%S %p')
|
|
return self.ct_tz.localize(dt).astimezone(self.utc_tz)
|
|
except ValueError:
|
|
try:
|
|
full_str = f"{date_str} {time_str}"
|
|
dt = datetime.strptime(full_str, '%m/%d/%Y %I:%M %p')
|
|
return self.ct_tz.localize(dt).astimezone(self.utc_tz)
|
|
except ValueError:
|
|
pass
|
|
|
|
return None
|
|
|
|
async def get_alerts_for_symbols(
|
|
self,
|
|
symbols: List[str],
|
|
time_window_start: datetime,
|
|
time_window_end: datetime
|
|
) -> pd.DataFrame:
|
|
"""Batch fetch alerts for multiple symbols within time window"""
|
|
if not symbols:
|
|
return pd.DataFrame()
|
|
|
|
async with self.pool.acquire() as conn:
|
|
# Build query with IN clause
|
|
placeholders = ','.join([f'${i+1}' for i in range(len(symbols))])
|
|
query = f"""
|
|
SELECT
|
|
"type",
|
|
"message",
|
|
"price",
|
|
"volume",
|
|
"date",
|
|
"timestamp",
|
|
"ticker"
|
|
FROM "AlertStream_monthly"
|
|
WHERE UPPER("ticker") IN ({placeholders})
|
|
"""
|
|
|
|
rows = await conn.fetch(query, *[s.upper() for s in symbols])
|
|
|
|
if not rows:
|
|
return pd.DataFrame()
|
|
|
|
# Convert to DataFrame
|
|
df = pd.DataFrame([dict(row) for row in rows])
|
|
|
|
# Parse timestamps
|
|
df['event_ts_utc'] = df.apply(
|
|
lambda row: self.parse_alert_timestamp(row['date'], row['timestamp']),
|
|
axis=1
|
|
)
|
|
|
|
# Filter by time window
|
|
df = df[
|
|
(df['event_ts_utc'] >= time_window_start) &
|
|
(df['event_ts_utc'] <= time_window_end)
|
|
].copy()
|
|
|
|
return df
|
|
|
|
async def match_alerts_to_flows(
|
|
self,
|
|
flow_df: pd.DataFrame
|
|
) -> pd.DataFrame:
|
|
"""Match alerts to flow data within ±15 minutes"""
|
|
df = flow_df.copy()
|
|
|
|
if df.empty:
|
|
return df
|
|
|
|
# Get unique symbols and time range
|
|
symbols = df['symbol_norm'].unique().tolist()
|
|
min_time = df['flow_ts_utc'].min() - timedelta(minutes=15)
|
|
max_time = df['flow_ts_utc'].max() + timedelta(minutes=15)
|
|
|
|
# Fetch all relevant alerts
|
|
alerts_df = await self.get_alerts_for_symbols(symbols, min_time, max_time)
|
|
|
|
if alerts_df.empty:
|
|
df['near_alert_type'] = None
|
|
df['near_alert_msg'] = None
|
|
df['near_alert_price'] = None
|
|
df['near_alert_volume'] = None
|
|
df['catalyst_flag'] = 0
|
|
return df
|
|
|
|
# Match alerts to flows
|
|
alert_data = []
|
|
|
|
for idx, flow_row in df.iterrows():
|
|
symbol = flow_row['symbol_norm']
|
|
flow_ts_utc = flow_row['flow_ts_utc']
|
|
|
|
if pd.isna(flow_ts_utc):
|
|
alert_data.append({
|
|
'near_alert_type': None,
|
|
'near_alert_msg': None,
|
|
'near_alert_price': None,
|
|
'near_alert_volume': None,
|
|
})
|
|
continue
|
|
|
|
# Filter alerts for this symbol within ±15 minutes
|
|
symbol_alerts = alerts_df[
|
|
(alerts_df['ticker'].str.upper() == symbol.upper()) &
|
|
(alerts_df['event_ts_utc'] >= flow_ts_utc - timedelta(minutes=15)) &
|
|
(alerts_df['event_ts_utc'] <= flow_ts_utc + timedelta(minutes=15))
|
|
]
|
|
|
|
if symbol_alerts.empty:
|
|
alert_data.append({
|
|
'near_alert_type': None,
|
|
'near_alert_msg': None,
|
|
'near_alert_price': None,
|
|
'near_alert_volume': None,
|
|
})
|
|
continue
|
|
|
|
# Find nearest alert by time difference
|
|
symbol_alerts['time_diff'] = abs(
|
|
(symbol_alerts['event_ts_utc'] - flow_ts_utc).dt.total_seconds()
|
|
)
|
|
nearest = symbol_alerts.loc[symbol_alerts['time_diff'].idxmin()]
|
|
|
|
alert_data.append({
|
|
'near_alert_type': nearest['type'],
|
|
'near_alert_msg': nearest['message'],
|
|
'near_alert_price': nearest['price'],
|
|
'near_alert_volume': nearest['volume'],
|
|
})
|
|
|
|
# Merge alert data
|
|
alert_df = pd.DataFrame(alert_data, index=df.index)
|
|
df = pd.concat([df, alert_df], axis=1)
|
|
|
|
# Add catalyst flag
|
|
df['catalyst_flag'] = df['near_alert_type'].notna().astype(int)
|
|
|
|
return df
|
|
|