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

94 lines
2.9 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"
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(f"💼 Scanning insider buying (last {self.lookback_days} days)...")
try:
# Use Finviz insider buying screener
from tradingagents.dataflows.finviz_scraper import get_finviz_insider_buying
result = get_finviz_insider_buying(
transaction_type="buy",
lookback_days=self.lookback_days,
min_value=self.min_transaction_value,
top_n=self.limit,
)
if not result or not isinstance(result, str):
logger.info("Found 0 insider purchases")
return []
# Parse the markdown result
candidates = []
seen_tickers = set()
# Extract tickers from markdown table
import re
lines = result.split("\n")
for line in lines:
if "|" not in line or "Ticker" in line or "---" in line:
continue
parts = [p.strip() for p in line.split("|")]
if len(parts) < 3:
continue
ticker = parts[1] if len(parts) > 1 else ""
ticker = ticker.strip().upper()
if not ticker or ticker in seen_tickers:
continue
# Validate ticker format
if not re.match(r"^[A-Z]{1,5}$", ticker):
continue
seen_tickers.add(ticker)
candidates.append(
{
"ticker": ticker,
"source": self.name,
"context": "Insider purchase detected (Finviz)",
"priority": Priority.HIGH.value,
"strategy": "insider_buying",
}
)
if len(candidates) >= self.limit:
break
logger.info(f"Found {len(candidates)} insider purchases")
return candidates
except Exception as e:
logger.warning(f"⚠️ Insider buying failed: {e}")
return []
SCANNER_REGISTRY.register(InsiderBuyingScanner)