30 KiB
30 KiB
Modular Multi-Pipeline Discovery Architecture - Fast Implementation
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Transform discovery system into modular, multi-pipeline architecture with early signal scanners, dynamic performance tracking, and Streamlit dashboard UI.
Approach: Implementation-first, skip tests/docs for fast experimentation.
Branch: feature/modular-pipeline-architecture (no git commits during implementation)
Phase 1: Core Architecture (30 min)
Task 1: Create Scanner Registry
Files:
- Create:
tradingagents/dataflows/discovery/scanner_registry.py
Implementation:
# tradingagents/dataflows/discovery/scanner_registry.py
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Type
class BaseScanner(ABC):
"""Base class for all discovery scanners."""
name: str = None
pipeline: str = None
def __init__(self, config: Dict[str, Any]):
if self.name is None:
raise ValueError(f"{self.__class__.__name__} must define 'name'")
if self.pipeline is None:
raise ValueError(f"{self.__class__.__name__} must define 'pipeline'")
self.config = config
self.scanner_config = config.get("discovery", {}).get("scanners", {}).get(self.name, {})
self.enabled = self.scanner_config.get("enabled", True)
self.limit = self.scanner_config.get("limit", 10)
@abstractmethod
def scan(self, state: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Return list of candidates with: ticker, source, context, priority"""
pass
def is_enabled(self) -> bool:
return self.enabled
class ScannerRegistry:
"""Global scanner registry."""
def __init__(self):
self.scanners: Dict[str, Type[BaseScanner]] = {}
def register(self, scanner_class: Type[BaseScanner]):
if not hasattr(scanner_class, "name") or scanner_class.name is None:
raise ValueError(f"Scanner must define 'name'")
if not hasattr(scanner_class, "pipeline") or scanner_class.pipeline is None:
raise ValueError(f"Scanner must define 'pipeline'")
self.scanners[scanner_class.name] = scanner_class
def get_scanners_by_pipeline(self, pipeline: str) -> List[Type[BaseScanner]]:
return [sc for sc in self.scanners.values() if sc.pipeline == pipeline]
def get_all_scanners(self) -> List[Type[BaseScanner]]:
return list(self.scanners.values())
SCANNER_REGISTRY = ScannerRegistry()
Task 2: Update Config with Modular Structure
Files:
- Modify:
tradingagents/default_config.py
Add to config:
"discovery": {
# ... existing settings ...
# PIPELINES: Define ranking behavior per pipeline
"pipelines": {
"edge": {
"enabled": True,
"priority": 1,
"ranker_prompt": "edge_signals_ranker.txt",
"deep_dive_budget": 15
},
"momentum": {
"enabled": True,
"priority": 2,
"ranker_prompt": "momentum_ranker.txt",
"deep_dive_budget": 10
},
"news": {
"enabled": True,
"priority": 3,
"ranker_prompt": "news_catalyst_ranker.txt",
"deep_dive_budget": 5
},
"social": {
"enabled": True,
"priority": 4,
"ranker_prompt": "social_signals_ranker.txt",
"deep_dive_budget": 5
},
"events": {
"enabled": False,
"priority": 5,
"deep_dive_budget": 0
}
},
# SCANNERS: Each declares its pipeline
"scanners": {
# Edge signals
"insider_buying": {"enabled": True, "pipeline": "edge", "limit": 20},
"options_flow": {"enabled": True, "pipeline": "edge", "limit": 15},
"congress_trades": {"enabled": False, "pipeline": "edge", "limit": 10},
# Momentum
"volume_accumulation": {"enabled": True, "pipeline": "momentum", "limit": 15},
"market_movers": {"enabled": True, "pipeline": "momentum", "limit": 10},
# News
"semantic_news": {"enabled": True, "pipeline": "news", "limit": 10},
"analyst_upgrade": {"enabled": False, "pipeline": "news", "limit": 5},
# Social
"reddit_trending": {"enabled": True, "pipeline": "social", "limit": 15},
"reddit_dd": {"enabled": True, "pipeline": "social", "limit": 10},
# Events
"earnings_calendar": {"enabled": False, "pipeline": "events", "limit": 10},
"short_squeeze": {"enabled": False, "pipeline": "events", "limit": 5}
}
}
Phase 2: New Edge Scanners (45 min)
Task 3: Insider Buying Scanner
Files:
- Create:
tradingagents/dataflows/discovery/scanners/insider_buying.py
Implementation:
# tradingagents/dataflows/discovery/scanners/insider_buying.py
"""SEC Form 4 insider buying scanner."""
import re
from datetime import datetime, timedelta
from typing import Any, Dict, List
from tradingagents.dataflows.discovery.scanner_registry import BaseScanner, SCANNER_REGISTRY
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)
def scan(self, state: Dict[str, Any]) -> List[Dict[str, Any]]:
if not self.is_enabled():
return []
print(f" 💼 Scanning insider buying (last {self.lookback_days} days)...")
try:
# Use existing FMP API or placeholder
# For MVP: Return empty or use FMP insider trades endpoint
candidates = []
# TODO: Implement actual Form 4 fetching
# For now, placeholder that uses FMP API if available
print(f" Found {len(candidates)} insider purchases")
return candidates
except Exception as e:
print(f" Error: {e}")
return []
SCANNER_REGISTRY.register(InsiderBuyingScanner)
Task 4: Options Flow Scanner
Files:
- Create:
tradingagents/dataflows/discovery/scanners/options_flow.py
Implementation:
# tradingagents/dataflows/discovery/scanners/options_flow.py
"""Unusual options activity scanner."""
from typing import Any, Dict, List
import yfinance as yf
from tradingagents.dataflows.discovery.scanner_registry import BaseScanner, SCANNER_REGISTRY
class OptionsFlowScanner(BaseScanner):
"""Scan for unusual options activity."""
name = "options_flow"
pipeline = "edge"
def __init__(self, config: Dict[str, Any]):
super().__init__(config)
self.min_volume_oi_ratio = self.scanner_config.get("min_volume_oi_ratio", 2.0)
# Focus on liquid options
self.ticker_universe = ["AAPL", "MSFT", "GOOGL", "AMZN", "META", "NVDA", "AMD", "TSLA"]
def scan(self, state: Dict[str, Any]) -> List[Dict[str, Any]]:
if not self.is_enabled():
return []
print(f" 📊 Scanning unusual options activity...")
candidates = []
for ticker in self.ticker_universe[:20]: # Limit for speed
try:
unusual = self._analyze_ticker_options(ticker)
if unusual:
candidates.append(unusual)
if len(candidates) >= self.limit:
break
except:
continue
print(f" Found {len(candidates)} unusual options flows")
return candidates
def _analyze_ticker_options(self, ticker: str) -> Dict[str, Any]:
try:
stock = yf.Ticker(ticker)
expirations = stock.options
if not expirations:
return None
options = stock.option_chain(expirations[0])
calls = options.calls
puts = options.puts
# Find unusual strikes
unusual_strikes = []
for _, opt in calls.iterrows():
vol = opt.get("volume", 0)
oi = opt.get("openInterest", 0)
if oi > 0 and vol > 1000 and (vol / oi) >= self.min_volume_oi_ratio:
unusual_strikes.append({
"type": "call",
"strike": opt["strike"],
"volume": vol,
"oi": oi
})
if not unusual_strikes:
return None
# Calculate P/C ratio
total_call_vol = calls["volume"].sum() if not calls.empty else 0
total_put_vol = puts["volume"].sum() if not puts.empty else 0
pc_ratio = total_put_vol / total_call_vol if total_call_vol > 0 else 0
sentiment = "bullish" if pc_ratio < 0.7 else "bearish" if pc_ratio > 1.3 else "neutral"
return {
"ticker": ticker,
"source": self.name,
"context": f"Unusual options: {len(unusual_strikes)} strikes, P/C={pc_ratio:.2f} ({sentiment})",
"priority": "high" if sentiment == "bullish" else "medium",
"strategy": "options_flow",
"put_call_ratio": round(pc_ratio, 2)
}
except:
return None
SCANNER_REGISTRY.register(OptionsFlowScanner)
Phase 3: Dynamic Performance Tracking (30 min)
Task 5: Position Tracker
Files:
- Create:
tradingagents/dataflows/discovery/performance/position_tracker.py
Implementation:
# tradingagents/dataflows/discovery/performance/position_tracker.py
"""Dynamic position tracking with time-series data."""
import json
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, List, Optional
class PositionTracker:
"""Track positions with continuous price monitoring."""
def __init__(self, data_dir: str = "data"):
self.data_dir = Path(data_dir)
self.tracking_dir = self.data_dir / "recommendations" / "tracking"
self.tracking_dir.mkdir(parents=True, exist_ok=True)
def create_position(self, recommendation: Dict[str, Any]) -> Dict[str, Any]:
"""Create new position to track."""
ticker = recommendation["ticker"]
entry_price = recommendation["entry_price"]
rec_date = recommendation.get("recommendation_date", datetime.now().isoformat())
return {
"ticker": ticker,
"recommendation_date": rec_date,
"entry_price": entry_price,
"pipeline": recommendation.get("pipeline", "unknown"),
"scanner": recommendation.get("scanner", "unknown"),
"strategy": recommendation.get("strategy_match", "unknown"),
"confidence": recommendation.get("confidence", 5),
"shares": recommendation.get("shares", 0),
"price_history": [{
"timestamp": rec_date,
"price": entry_price,
"return_pct": 0.0,
"hours_held": 0,
"days_held": 0
}],
"metrics": {
"peak_return": 0.0,
"current_return": 0.0,
"days_held": 0,
"status": "open"
}
}
def update_position_price(self, position: Dict[str, Any], new_price: float,
timestamp: Optional[str] = None) -> Dict[str, Any]:
"""Update position with new price."""
if timestamp is None:
timestamp = datetime.now().isoformat()
entry_price = position["entry_price"]
entry_time = datetime.fromisoformat(position["recommendation_date"])
current_time = datetime.fromisoformat(timestamp)
return_pct = ((new_price - entry_price) / entry_price) * 100.0
time_diff = current_time - entry_time
hours_held = time_diff.total_seconds() / 3600
days_held = time_diff.days
position["price_history"].append({
"timestamp": timestamp,
"price": new_price,
"return_pct": round(return_pct, 2),
"hours_held": round(hours_held, 1),
"days_held": days_held
})
# Update metrics
position["metrics"]["current_return"] = round(return_pct, 2)
position["metrics"]["current_price"] = new_price
position["metrics"]["days_held"] = days_held
position["metrics"]["peak_return"] = max(
position["metrics"]["peak_return"],
return_pct
)
return position
def save_position(self, position: Dict[str, Any]) -> None:
"""Save position to disk."""
ticker = position["ticker"]
rec_date = position["recommendation_date"].split("T")[0]
filename = f"{ticker}_{rec_date}.json"
filepath = self.tracking_dir / filename
with open(filepath, "w") as f:
json.dump(position, f, indent=2)
def load_all_open_positions(self) -> List[Dict[str, Any]]:
"""Load all open positions."""
positions = []
for filepath in self.tracking_dir.glob("*.json"):
with open(filepath, "r") as f:
position = json.load(f)
if position["metrics"]["status"] == "open":
positions.append(position)
return positions
Task 6: Position Updater Script
Files:
- Create:
scripts/update_positions.py
Implementation:
# scripts/update_positions.py
"""Update all open positions with current prices."""
import yfinance as yf
from datetime import datetime
from tradingagents.dataflows.discovery.performance.position_tracker import PositionTracker
def main():
tracker = PositionTracker()
positions = tracker.load_all_open_positions()
if not positions:
print("No open positions")
return
print(f"Updating {len(positions)} positions...")
# Get unique tickers
tickers = list(set(p["ticker"] for p in positions))
# Fetch prices
try:
tickers_str = " ".join(tickers)
data = yf.download(tickers_str, period="1d", progress=False)
prices = {}
if len(tickers) == 1:
prices[tickers[0]] = float(data["Close"].iloc[-1])
else:
for ticker in tickers:
try:
prices[ticker] = float(data["Close"][ticker].iloc[-1])
except:
pass
# Update each position
for position in positions:
ticker = position["ticker"]
if ticker in prices:
updated = tracker.update_position_price(position, prices[ticker])
tracker.save_position(updated)
print(f" {ticker}: ${prices[ticker]:.2f} ({updated['metrics']['current_return']:+.1f}%)")
print(f"✅ Updated {len(positions)} positions")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()
Phase 4: Streamlit Dashboard (60 min)
Task 7: Install Dependencies & Create Entry Point
Files:
- Update:
requirements.txt - Create:
tradingagents/ui/dashboard.py - Create:
tradingagents/ui/utils.py - Create:
tradingagents/ui/pages/__init__.py
Add to requirements.txt:
streamlit>=1.40.0
plotly>=5.18.0
Dashboard entry point:
# tradingagents/ui/dashboard.py
"""Trading Discovery Dashboard."""
import streamlit as st
st.set_page_config(
page_title="Trading Discovery",
page_icon="🎯",
layout="wide"
)
from tradingagents.ui.pages import home, todays_picks, portfolio, performance, settings
def main():
st.sidebar.title("🎯 Trading Discovery")
page = st.sidebar.radio(
"Navigation",
["Home", "Today's Picks", "Portfolio", "Performance", "Settings"]
)
# Quick stats
st.sidebar.markdown("---")
st.sidebar.markdown("### Quick Stats")
try:
from tradingagents.ui.utils import load_quick_stats
stats = load_quick_stats()
st.sidebar.metric("Open Positions", stats.get("open_positions", 0))
st.sidebar.metric("Win Rate", f"{stats.get('win_rate_7d', 0):.1f}%")
except:
pass
# Render page
if page == "Home":
home.render()
elif page == "Today's Picks":
todays_picks.render()
elif page == "Portfolio":
portfolio.render()
elif page == "Performance":
performance.render()
elif page == "Settings":
settings.render()
if __name__ == "__main__":
main()
Utils:
# tradingagents/ui/utils.py
"""Dashboard utilities."""
import json
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List
def load_statistics() -> Dict[str, Any]:
"""Load performance statistics."""
stats_file = Path("data/recommendations/statistics.json")
if not stats_file.exists():
return {}
with open(stats_file, "r") as f:
return json.load(f)
def load_recommendations(date: str = None) -> List[Dict[str, Any]]:
"""Load recommendations for date."""
if date is None:
date = datetime.now().strftime("%Y-%m-%d")
rec_file = Path(f"data/recommendations/{date}_recommendations.json")
if not rec_file.exists():
return []
with open(rec_file, "r") as f:
data = json.load(f)
return data.get("rankings", [])
def load_open_positions() -> List[Dict[str, Any]]:
"""Load all open positions."""
from tradingagents.dataflows.discovery.performance.position_tracker import PositionTracker
tracker = PositionTracker()
return tracker.load_all_open_positions()
def load_quick_stats() -> Dict[str, Any]:
"""Load sidebar quick stats."""
stats = load_statistics()
positions = load_open_positions()
return {
"open_positions": len(positions),
"win_rate_7d": stats.get("overall_7d", {}).get("win_rate", 0)
}
Task 8: Home Page
Files:
- Create:
tradingagents/ui/pages/home.py
# tradingagents/ui/pages/home.py
"""Home page."""
import streamlit as st
import plotly.express as px
import pandas as pd
from tradingagents.ui.utils import load_statistics, load_open_positions
def render():
st.title("🎯 Trading Discovery Dashboard")
stats = load_statistics()
if not stats:
st.warning("No data. Run discovery first.")
return
# Metrics
col1, col2, col3, col4 = st.columns(4)
overall_7d = stats.get("overall_7d", {})
with col1:
st.metric("Win Rate (7d)", f"{overall_7d.get('win_rate', 0):.1f}%")
with col2:
st.metric("Open Positions", len(load_open_positions()))
with col3:
st.metric("Avg Return (7d)", f"{overall_7d.get('avg_return', 0):+.1f}%")
with col4:
by_pipeline = stats.get("by_pipeline", {})
if by_pipeline:
best = max(by_pipeline.items(), key=lambda x: x[1].get("win_rate_7d", 0))
st.metric("Best Pipeline", f"{best[0].title()} ({best[1].get('win_rate_7d', 0):.0f}%)")
# Pipeline chart
st.subheader("📊 Pipeline Performance")
if by_pipeline:
data = []
for pipeline, d in by_pipeline.items():
data.append({
"Pipeline": pipeline.title(),
"Win Rate": d.get("win_rate_7d", 0),
"Avg Return": d.get("avg_return_7d", 0),
"Count": d.get("count", 0)
})
df = pd.DataFrame(data)
fig = px.scatter(df, x="Win Rate", y="Avg Return", size="Count", color="Pipeline",
title="Pipeline Performance")
fig.add_hline(y=0, line_dash="dash")
fig.add_vline(x=50, line_dash="dash")
st.plotly_chart(fig, use_container_width=True)
Task 9: Today's Picks Page
Files:
- Create:
tradingagents/ui/pages/todays_picks.py
# tradingagents/ui/pages/todays_picks.py
"""Today's recommendations."""
import streamlit as st
from datetime import datetime
from tradingagents.ui.utils import load_recommendations
def render():
st.title("📋 Today's Recommendations")
today = datetime.now().strftime("%Y-%m-%d")
recommendations = load_recommendations(today)
if not recommendations:
st.warning(f"No recommendations for {today}")
return
# Filters
col1, col2, col3 = st.columns(3)
with col1:
pipelines = list(set(r.get("pipeline", "unknown") for r in recommendations))
pipeline_filter = st.multiselect("Pipeline", pipelines, default=pipelines)
with col2:
min_confidence = st.slider("Min Confidence", 1, 10, 7)
with col3:
min_score = st.slider("Min Score", 0, 100, 70)
# Apply filters
filtered = [r for r in recommendations
if r.get("pipeline", "unknown") in pipeline_filter
and r.get("confidence", 0) >= min_confidence
and r.get("final_score", 0) >= min_score]
st.write(f"**{len(filtered)}** of **{len(recommendations)}** recommendations")
# Display recommendations
for i, rec in enumerate(filtered, 1):
ticker = rec.get("ticker", "UNKNOWN")
score = rec.get("final_score", 0)
confidence = rec.get("confidence", 0)
with st.expander(f"#{i} {ticker} - {rec.get('company_name', '')} (Score: {score}, Conf: {confidence}/10)"):
col1, col2 = st.columns([2, 1])
with col1:
st.write(f"**Pipeline:** {rec.get('pipeline', 'unknown').title()}")
st.write(f"**Scanner:** {rec.get('scanner', 'unknown')}")
st.write(f"**Price:** ${rec.get('current_price', 0):.2f}")
st.write(f"**Thesis:** {rec.get('reason', 'N/A')}")
with col2:
if st.button("✅ Enter Position", key=f"enter_{ticker}"):
st.info("Position entry modal (TODO)")
if st.button("👀 Watch", key=f"watch_{ticker}"):
st.success(f"Added {ticker} to watchlist")
Task 10: Portfolio Page
Files:
- Create:
tradingagents/ui/pages/portfolio.py
# tradingagents/ui/pages/portfolio.py
"""Portfolio tracker."""
import streamlit as st
import plotly.express as px
import pandas as pd
from datetime import datetime
from tradingagents.ui.utils import load_open_positions
def render():
st.title("💼 Portfolio Tracker")
# Manual add form
with st.expander("➕ Add Position"):
col1, col2, col3, col4 = st.columns(4)
with col1:
ticker = st.text_input("Ticker")
with col2:
entry_price = st.number_input("Entry Price", min_value=0.0)
with col3:
shares = st.number_input("Shares", min_value=0, step=1)
with col4:
st.write("") # Spacing
if st.button("Add"):
if ticker and entry_price > 0 and shares > 0:
from tradingagents.dataflows.discovery.performance.position_tracker import PositionTracker
tracker = PositionTracker()
pos = tracker.create_position({
"ticker": ticker.upper(),
"entry_price": entry_price,
"shares": shares,
"recommendation_date": datetime.now().isoformat(),
"pipeline": "manual",
"scanner": "manual",
"strategy_match": "manual",
"confidence": 5
})
tracker.save_position(pos)
st.success(f"Added {ticker.upper()}")
st.rerun()
# Load positions
positions = load_open_positions()
if not positions:
st.info("No open positions")
return
# Summary
total_invested = sum(p["entry_price"] * p.get("shares", 0) for p in positions)
total_current = sum(p["metrics"]["current_price"] * p.get("shares", 0) for p in positions)
total_pnl = total_current - total_invested
total_pnl_pct = (total_pnl / total_invested * 100) if total_invested > 0 else 0
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Invested", f"${total_invested:,.0f}")
with col2:
st.metric("Current", f"${total_current:,.0f}")
with col3:
st.metric("P/L", f"${total_pnl:,.0f}", delta=f"{total_pnl_pct:+.1f}%")
with col4:
st.metric("Positions", len(positions))
# Table
st.subheader("📊 Positions")
data = []
for p in positions:
pnl = (p["metrics"]["current_price"] - p["entry_price"]) * p.get("shares", 0)
data.append({
"Ticker": p["ticker"],
"Entry": f"${p['entry_price']:.2f}",
"Current": f"${p['metrics']['current_price']:.2f}",
"Shares": p.get("shares", 0),
"P/L": f"${pnl:.2f}",
"P/L %": f"{p['metrics']['current_return']:+.1f}%",
"Days": p["metrics"]["days_held"]
})
df = pd.DataFrame(data)
st.dataframe(df, use_container_width=True)
Task 11: Performance & Settings Pages (Simplified)
Files:
- Create:
tradingagents/ui/pages/performance.py - Create:
tradingagents/ui/pages/settings.py
# tradingagents/ui/pages/performance.py
"""Performance analytics."""
import streamlit as st
import plotly.express as px
import pandas as pd
from tradingagents.ui.utils import load_statistics
def render():
st.title("📊 Performance Analytics")
stats = load_statistics()
if not stats:
st.warning("No data available")
return
# Scanner heatmap
st.subheader("🔥 Scanner Performance")
by_scanner = stats.get("by_scanner", {})
if by_scanner:
data = []
for scanner, d in by_scanner.items():
data.append({
"Scanner": scanner,
"Win Rate": d.get("win_rate_7d", 0),
"Avg Return": d.get("avg_return_7d", 0),
"Count": d.get("count", 0)
})
df = pd.DataFrame(data)
fig = px.scatter(df, x="Win Rate", y="Avg Return", size="Count",
color="Win Rate", hover_data=["Scanner"],
color_continuous_scale="RdYlGn")
fig.add_hline(y=0, line_dash="dash")
fig.add_vline(x=50, line_dash="dash")
st.plotly_chart(fig, use_container_width=True)
# tradingagents/ui/pages/settings.py
"""Settings page."""
import streamlit as st
from tradingagents.default_config import DEFAULT_CONFIG
def render():
st.title("⚙️ Settings")
st.info("Configuration UI - TODO: Implement save functionality")
# Show current config
config = DEFAULT_CONFIG.get("discovery", {})
st.subheader("Pipelines")
pipelines = config.get("pipelines", {})
for name, cfg in pipelines.items():
with st.expander(f"{name.title()} Pipeline"):
st.write(f"Enabled: {cfg.get('enabled')}")
st.write(f"Priority: {cfg.get('priority')}")
st.write(f"Budget: {cfg.get('deep_dive_budget')}")
st.subheader("Scanners")
scanners = config.get("scanners", {})
for name, cfg in scanners.items():
st.checkbox(f"{name}", value=cfg.get("enabled"), key=f"scan_{name}")
Create init.py:
# tradingagents/ui/pages/__init__.py
from . import home, todays_picks, portfolio, performance, settings
Phase 5: Integration (15 min)
Task 12: Update Discovery Graph
Files:
- Modify:
tradingagents/graph/discovery_graph.py
Add to imports:
from tradingagents.dataflows.discovery.scanner_registry import SCANNER_REGISTRY
Replace scanner_node() method:
def scanner_node(self, state: DiscoveryState) -> Dict[str, Any]:
"""Scan using modular registry."""
print("🔍 Scanning market for opportunities...")
# Performance tracking
try:
self.analytics.update_performance_tracking()
except Exception as e:
print(f" Warning: {e}")
state.setdefault("tool_logs", [])
# Collect by pipeline
pipeline_candidates = {
"edge": [], "momentum": [], "news": [], "social": [], "events": []
}
pipeline_config = self.config.get("discovery", {}).get("pipelines", {})
# Run enabled scanners
for scanner_class in SCANNER_REGISTRY.get_all_scanners():
pipeline = scanner_class.pipeline
if not pipeline_config.get(pipeline, {}).get("enabled", True):
continue
try:
scanner = scanner_class(self.config)
if not scanner.is_enabled():
continue
state["tool_executor"] = self._execute_tool_logged
candidates = scanner.scan(state)
pipeline_candidates[pipeline].extend(candidates)
except Exception as e:
print(f" Error in {scanner_class.name}: {e}")
# Merge candidates
all_candidates = []
for candidates in pipeline_candidates.values():
all_candidates.extend(candidates)
unique_candidates = {}
self._merge_candidates_into_dict(all_candidates, unique_candidates)
final = list(unique_candidates.values())
print(f" Found {len(final)} unique candidates")
return {
"tickers": [c["ticker"] for c in final],
"candidate_metadata": final,
"tool_logs": state.get("tool_logs", []),
"status": "scanned"
}
Summary & Running
What's Implemented:
- ✅ Modular scanner registry
- ✅ Config with pipelines/scanners
- ✅ 2 edge scanners (insider, options) as templates
- ✅ Dynamic position tracker
- ✅ Position updater script
- ✅ Full Streamlit dashboard (5 pages)
- ✅ Discovery graph integration
To Run:
# Install dependencies
pip install streamlit plotly
# Update positions (run hourly)
python scripts/update_positions.py
# Start dashboard
streamlit run tradingagents/ui/dashboard.py
# Run discovery
python -m cli.main analyze # Select discovery mode
Next Steps:
- Test discovery with new architecture
- Add more edge scanners (congress, 13F)
- Add tests/docs when ready
- Tune scanner limits based on performance