TradingAgents/tradingagents/dataflows/discovery/scanners/insider_buying.py

119 lines
4.3 KiB
Python

"""SEC Form 4 insider buying scanner."""
from typing import Any, Dict, List
from tradingagents.dataflows.discovery.scanner_registry import SCANNER_REGISTRY, BaseScanner
from tradingagents.dataflows.discovery.utils import Priority
from tradingagents.utils.logger import get_logger
logger = get_logger(__name__)
class InsiderBuyingScanner(BaseScanner):
"""Scan SEC Form 4 for insider purchases."""
name = "insider_buying"
pipeline = "edge"
strategy = "insider_buying"
def __init__(self, config: Dict[str, Any]):
super().__init__(config)
self.lookback_days = self.scanner_config.get("lookback_days", 7)
self.min_transaction_value = self.scanner_config.get("min_transaction_value", 25000)
def scan(self, state: Dict[str, Any]) -> List[Dict[str, Any]]:
if not self.is_enabled():
return []
logger.info("Scanning insider buying (OpenInsider)...")
try:
from tradingagents.dataflows.finviz_scraper import get_finviz_insider_buying
transactions = get_finviz_insider_buying(
lookback_days=self.lookback_days,
min_value=self.min_transaction_value,
return_structured=True,
deduplicate=False,
)
if not transactions:
logger.info("No insider buying transactions found")
return []
logger.info(f"Found {len(transactions)} insider transactions")
# Group by ticker for cluster detection
by_ticker: Dict[str, list] = {}
for txn in transactions:
ticker = txn.get("ticker", "").upper().strip()
if not ticker:
continue
by_ticker.setdefault(ticker, []).append(txn)
candidates = []
for ticker, txns in by_ticker.items():
# Use the largest transaction as primary
txns.sort(key=lambda t: t.get("value_num", 0), reverse=True)
primary = txns[0]
insider_name = primary.get("insider", "Unknown")
title = primary.get("title", "")
value = primary.get("value_num", 0)
value_str = primary.get("value_str", f"${value:,.0f}")
num_insiders = len(set(t.get("insider", "") for t in txns))
# Priority by significance
title_lower = title.lower()
is_c_suite = any(
t in title_lower for t in ["ceo", "cfo", "coo", "cto", "president", "chairman"]
)
is_director = "director" in title_lower
if num_insiders >= 2:
priority = Priority.CRITICAL.value
elif is_c_suite and value >= 100_000:
priority = Priority.CRITICAL.value
elif is_c_suite or (is_director and value >= 50_000):
priority = Priority.HIGH.value
elif value >= 50_000:
priority = Priority.HIGH.value
else:
priority = Priority.MEDIUM.value
# Build context
if num_insiders > 1:
context = (
f"Cluster: {num_insiders} insiders buying {ticker}. "
f"Largest: {title} {insider_name} purchased {value_str}"
)
else:
context = f"{title} {insider_name} purchased {value_str} of {ticker}"
candidates.append(
{
"ticker": ticker,
"source": self.name,
"context": context,
"priority": priority,
"strategy": self.strategy,
"insider_name": insider_name,
"insider_title": title,
"transaction_value": value,
"num_insiders_buying": num_insiders,
}
)
if len(candidates) >= self.limit:
break
logger.info(f"Insider buying: {len(candidates)} candidates")
return candidates
except Exception as e:
logger.error(f"Insider buying scan failed: {e}", exc_info=True)
return []
SCANNER_REGISTRY.register(InsiderBuyingScanner)