98 lines
3.2 KiB
Python
98 lines
3.2 KiB
Python
"""Analyst upgrade and initiation 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 AnalystUpgradeScanner(BaseScanner):
|
|
"""Scan for recent analyst upgrades and coverage initiations."""
|
|
|
|
name = "analyst_upgrades"
|
|
pipeline = "edge"
|
|
strategy = "analyst_upgrade"
|
|
|
|
def __init__(self, config: Dict[str, Any]):
|
|
super().__init__(config)
|
|
self.lookback_days = self.scanner_config.get("lookback_days", 3)
|
|
self.max_hours_old = self.scanner_config.get("max_hours_old", 72)
|
|
|
|
def scan(self, state: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
if not self.is_enabled():
|
|
return []
|
|
|
|
logger.info("📊 Scanning analyst upgrades and initiations...")
|
|
|
|
try:
|
|
from tradingagents.dataflows.alpha_vantage_analysts import (
|
|
get_analyst_rating_changes,
|
|
)
|
|
|
|
changes = get_analyst_rating_changes(
|
|
lookback_days=self.lookback_days,
|
|
change_types=["upgrade", "initiated"],
|
|
top_n=self.limit * 2,
|
|
return_structured=True,
|
|
)
|
|
|
|
if not changes:
|
|
logger.info("No analyst upgrades found")
|
|
return []
|
|
|
|
candidates = []
|
|
for change in changes:
|
|
ticker = change.get("ticker", "").upper().strip()
|
|
if not ticker:
|
|
continue
|
|
|
|
action = change.get("action", "unknown")
|
|
hours_old = change.get("hours_old", 999)
|
|
headline = change.get("headline", "")
|
|
source = change.get("source", "")
|
|
|
|
if hours_old > self.max_hours_old:
|
|
continue
|
|
|
|
# Priority by freshness and action type
|
|
if action == "upgrade" and hours_old <= 24:
|
|
priority = Priority.HIGH.value
|
|
elif action == "initiated" and hours_old <= 24:
|
|
priority = Priority.HIGH.value
|
|
elif hours_old <= 48:
|
|
priority = Priority.MEDIUM.value
|
|
else:
|
|
priority = Priority.LOW.value
|
|
|
|
context = (
|
|
f"Analyst {action}: {headline}" if headline else f"Analyst {action} ({source})"
|
|
)
|
|
|
|
candidates.append(
|
|
{
|
|
"ticker": ticker,
|
|
"source": self.name,
|
|
"context": context,
|
|
"priority": priority,
|
|
"strategy": self.strategy,
|
|
"analyst_action": action,
|
|
"hours_old": hours_old,
|
|
}
|
|
)
|
|
|
|
if len(candidates) >= self.limit:
|
|
break
|
|
|
|
logger.info(f"Analyst upgrades: {len(candidates)} candidates")
|
|
return candidates
|
|
|
|
except Exception as e:
|
|
logger.error(f"Analyst upgrades scan failed: {e}", exc_info=True)
|
|
return []
|
|
|
|
|
|
SCANNER_REGISTRY.register(AnalystUpgradeScanner)
|