smaug/alerts/slack_alert.py
Claude 7e9221a914 feat: add PLAN.md and insider copytrade POC implementation
- PLAN.md: full implementation plan from issue
- config.py: configurable thresholds, API keys via .env
- ingestion/: EDGAR RSS poller + Form 4 XML parser
- db/: SQLite schema + interface (WAL mode)
- signals/: filter engine (buy/10b5-1/value/role) + cluster detector
- alerts/: Slack webhook alert with score gating
- broker/: Alpaca paper/live trade execution
- backtest/: historical signal backtesting with yfinance
- main.py: CLI entrypoint (run | fetch-once | backtest)
2026-05-04 16:15:22 +00:00

65 lines
2.2 KiB
Python

import logging
import requests
import config
from db.db import mark_signal_alerted
logger = logging.getLogger(__name__)
def format_signal(signal: dict) -> str:
filing = signal.get("filing", {})
ticker = signal["ticker"]
insider = filing.get("insider_name", "Unknown")
role = filing.get("role", "Unknown")
tx_date = filing.get("transaction_date", "")
shares = filing.get("shares")
price = filing.get("price")
total_value = filing.get("total_value") or signal.get("total_cluster_value", 0)
cluster_size = signal["cluster_size"]
score = signal["score"]
is_10b51 = "Yes" if filing.get("is_10b51") else "No"
accession = filing.get("accession_number", "")
shares_str = f"{shares:,.0f}" if shares else "N/A"
price_str = f"${price:,.2f}" if price else "N/A"
value_str = f"${total_value:,.0f}" if total_value else "N/A"
edgar_url = f"https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&type=4&dateb=&owner=include&count=10&search_text=&ticker={ticker}"
return (
f"*INSIDER BUY SIGNAL*\n"
f"Ticker: ${ticker}\n"
f"Insider: {insider} ({role})\n"
f"Date: {tx_date}\n"
f"Shares: {shares_str} @ {price_str} = {value_str}\n"
f"Cluster: {cluster_size} insider(s) in last {config.CLUSTER_WINDOW_DAYS} days\n"
f"Score: {score}\n"
f"10b5-1: {is_10b51}\n"
f"EDGAR: {edgar_url}"
)
def send_slack_alert(signal: dict) -> bool:
if not config.SLACK_WEBHOOK_URL:
logger.warning("SLACK_WEBHOOK_URL not configured")
return False
if signal.get("score", 0) < config.SCORE_ALERT_THRESHOLD:
logger.debug(f"Signal score {signal['score']} below threshold {config.SCORE_ALERT_THRESHOLD}")
return False
text = format_signal(signal)
try:
resp = requests.post(
config.SLACK_WEBHOOK_URL,
json={"text": text},
timeout=10,
)
resp.raise_for_status()
mark_signal_alerted(signal["id"])
logger.info(f"Slack alert sent for {signal['ticker']}")
return True
except Exception as e:
logger.error(f"Failed to send Slack alert: {e}")
return False