diff --git a/.gitignore b/.gitignore index 9a2904a9..281b183f 100644 --- a/.gitignore +++ b/.gitignore @@ -217,3 +217,10 @@ __marimo__/ # Cache **/data_cache/ + +# Scan results and execution plans (generated artifacts) +results/ +plans/ + +# Backup files +*.backup diff --git a/CLAUDE.md b/CLAUDE.md index e6b6f6ba..c94ee85c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,12 +6,12 @@ Multi-agent LLM trading framework using LangGraph for financial analysis and dec ## Development Environment -**Conda Environment**: `trasingagetns` +**Conda Environment**: `tradingagents` Before starting any development work, activate the conda environment: ```bash -conda activate trasingagetns +conda activate tradingagents ``` ## Architecture diff --git a/cli/main.py b/cli/main.py index ad9cc1e9..0d78c9f3 100644 --- a/cli/main.py +++ b/cli/main.py @@ -1178,6 +1178,30 @@ def run_analysis(): display_complete_report(final_state) +def _is_scanner_error(result: str) -> bool: + """Return True when *result* indicates an error or missing data from a scanner tool.""" + error_prefixes = ( + "Error", + "No data", + "No quotes", + "No movers", + "No news", + "No industry", + "Invalid", + "Alpha Vantage", + ) + return any(result.startswith(prefix) for prefix in error_prefixes) + + +def _invoke_and_save(tool, args: dict, save_dir: Path, filename: str, label: str) -> str: + """Invoke a scanner tool, print a preview, and save the result if it is valid.""" + result = tool.invoke(args) + if not _is_scanner_error(result): + (save_dir / filename).write_text(result) + console.print(result[:500] + "..." if len(result) > 500 else result) + return result + + def run_scan(): console.print(Panel("[bold green]Global Macro Scanner[/bold green]", border_style="green")) default_date = datetime.datetime.now().strftime("%Y-%m-%d") @@ -1190,35 +1214,20 @@ def run_scan(): # Call scanner tools console.print("[bold]1. Market Movers[/bold]") - movers = get_market_movers.invoke({"category": "day_gainers"}) - if not (movers.startswith("Error") or movers.startswith("No data")): - (save_dir / "market_movers.txt").write_text(movers) - console.print(movers[:500] + "..." if len(movers) > 500 else movers) - + _invoke_and_save(get_market_movers, {"category": "day_gainers"}, save_dir, "market_movers.txt", "Market Movers") + console.print("[bold]2. Market Indices[/bold]") - indices = get_market_indices.invoke({}) - if not (indices.startswith("Error") or indices.startswith("No data")): - (save_dir / "market_indices.txt").write_text(indices) - console.print(indices[:500] + "..." if len(indices) > 500 else indices) - + _invoke_and_save(get_market_indices, {}, save_dir, "market_indices.txt", "Market Indices") + console.print("[bold]3. Sector Performance[/bold]") - sectors = get_sector_performance.invoke({}) - if not (sectors.startswith("Error") or sectors.startswith("No data")): - (save_dir / "sector_performance.txt").write_text(sectors) - console.print(sectors[:500] + "..." if len(sectors) > 500 else sectors) - + _invoke_and_save(get_sector_performance, {}, save_dir, "sector_performance.txt", "Sector Performance") + console.print("[bold]4. Industry Performance (Technology)[/bold]") - industry = get_industry_performance.invoke({"sector_key": "technology"}) - if not (industry.startswith("Error") or industry.startswith("No data")): - (save_dir / "industry_performance.txt").write_text(industry) - console.print(industry[:500] + "..." if len(industry) > 500 else industry) - + _invoke_and_save(get_industry_performance, {"sector_key": "technology"}, save_dir, "industry_performance.txt", "Industry Performance") + console.print("[bold]5. Topic News (Market)[/bold]") - news = get_topic_news.invoke({"topic": "market", "limit": 10}) - if not (news.startswith("Error") or news.startswith("No data")): - (save_dir / "topic_news.txt").write_text(news) - console.print(news[:500] + "..." if len(news) > 500 else news) - + _invoke_and_save(get_topic_news, {"topic": "market", "limit": 10}, save_dir, "topic_news.txt", "Topic News") + console.print(f"[green]Results saved to {save_dir}[/green]") diff --git a/cli/main.py.backup b/cli/main.py.backup deleted file mode 100644 index 45673a1d..00000000 --- a/cli/main.py.backup +++ /dev/null @@ -1,1236 +0,0 @@ -from typing import Optional -import datetime -import typer -from pathlib import Path -from functools import wraps -from rich.console import Console -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() -from rich.panel import Panel -from rich.spinner import Spinner -from rich.live import Live -from rich.columns import Columns -from rich.markdown import Markdown -from rich.layout import Layout -from rich.text import Text -from rich.table import Table -from collections import deque -import time -from rich.tree import Tree -from rich import box -from rich.align import Align -from rich.rule import Rule - -from tradingagents.graph.trading_graph import TradingAgentsGraph -from tradingagents.default_config import DEFAULT_CONFIG -from cli.models import AnalystType -from cli.utils import * -from tradingagents.agents.utils.scanner_tools import ( - get_market_movers, - get_market_indices, - get_sector_performance, - get_industry_performance, - get_topic_news, -) -from cli.announcements import fetch_announcements, display_announcements -from cli.stats_handler import StatsCallbackHandler - -console = Console() - -app = typer.Typer( - name="TradingAgents", - help="TradingAgents CLI: Multi-Agents LLM Financial Trading Framework", - add_completion=True, # Enable shell completion -) - - -# Create a deque to store recent messages with a maximum length -class MessageBuffer: - # Fixed teams that always run (not user-selectable) - FIXED_AGENTS = { - "Research Team": ["Bull Researcher", "Bear Researcher", "Research Manager"], - "Trading Team": ["Trader"], - "Risk Management": ["Aggressive Analyst", "Neutral Analyst", "Conservative Analyst"], - "Portfolio Management": ["Portfolio Manager"], - } - - # Analyst name mapping - ANALYST_MAPPING = { - "market": "Market Analyst", - "social": "Social Analyst", - "news": "News Analyst", - "fundamentals": "Fundamentals Analyst", - } - - # Report section mapping: section -> (analyst_key for filtering, finalizing_agent) - # analyst_key: which analyst selection controls this section (None = always included) - # finalizing_agent: which agent must be "completed" for this report to count as done - REPORT_SECTIONS = { - "market_report": ("market", "Market Analyst"), - "sentiment_report": ("social", "Social Analyst"), - "news_report": ("news", "News Analyst"), - "fundamentals_report": ("fundamentals", "Fundamentals Analyst"), - "investment_plan": (None, "Research Manager"), - "trader_investment_plan": (None, "Trader"), - "final_trade_decision": (None, "Portfolio Manager"), - } - - def __init__(self, max_length=100): - self.messages = deque(maxlen=max_length) - self.tool_calls = deque(maxlen=max_length) - self.current_report = None - self.final_report = None # Store the complete final report - self.agent_status = {} - self.current_agent = None - self.report_sections = {} - self.selected_analysts = [] - self._last_message_id = None - - def init_for_analysis(self, selected_analysts): - """Initialize agent status and report sections based on selected analysts. - - Args: - selected_analysts: List of analyst type strings (e.g., ["market", "news"]) - """ - self.selected_analysts = [a.lower() for a in selected_analysts] - - # Build agent_status dynamically - self.agent_status = {} - - # Add selected analysts - for analyst_key in self.selected_analysts: - if analyst_key in self.ANALYST_MAPPING: - self.agent_status[self.ANALYST_MAPPING[analyst_key]] = "pending" - - # Add fixed teams - for team_agents in self.FIXED_AGENTS.values(): - for agent in team_agents: - self.agent_status[agent] = "pending" - - # Build report_sections dynamically - self.report_sections = {} - for section, (analyst_key, _) in self.REPORT_SECTIONS.items(): - if analyst_key is None or analyst_key in self.selected_analysts: - self.report_sections[section] = None - - # Reset other state - self.current_report = None - self.final_report = None - self.current_agent = None - self.messages.clear() - self.tool_calls.clear() - self._last_message_id = None - - def get_completed_reports_count(self): - """Count reports that are finalized (their finalizing agent is completed). - - A report is considered complete when: - 1. The report section has content (not None), AND - 2. The agent responsible for finalizing that report has status "completed" - - This prevents interim updates (like debate rounds) from counting as completed. - """ - count = 0 - for section in self.report_sections: - if section not in self.REPORT_SECTIONS: - continue - _, finalizing_agent = self.REPORT_SECTIONS[section] - # Report is complete if it has content AND its finalizing agent is done - has_content = self.report_sections.get(section) is not None - agent_done = self.agent_status.get(finalizing_agent) == "completed" - if has_content and agent_done: - count += 1 - return count - - def add_message(self, message_type, content): - timestamp = datetime.datetime.now().strftime("%H:%M:%S") - self.messages.append((timestamp, message_type, content)) - - def add_tool_call(self, tool_name, args): - timestamp = datetime.datetime.now().strftime("%H:%M:%S") - self.tool_calls.append((timestamp, tool_name, args)) - - def update_agent_status(self, agent, status): - if agent in self.agent_status: - self.agent_status[agent] = status - self.current_agent = agent - - def update_report_section(self, section_name, content): - if section_name in self.report_sections: - self.report_sections[section_name] = content - self._update_current_report() - - def _update_current_report(self): - # For the panel display, only show the most recently updated section - latest_section = None - latest_content = None - - # Find the most recently updated section - for section, content in self.report_sections.items(): - if content is not None: - latest_section = section - latest_content = content - - if latest_section and latest_content: - # Format the current section for display - section_titles = { - "market_report": "Market Analysis", - "sentiment_report": "Social Sentiment", - "news_report": "News Analysis", - "fundamentals_report": "Fundamentals Analysis", - "investment_plan": "Research Team Decision", - "trader_investment_plan": "Trading Team Plan", - "final_trade_decision": "Portfolio Management Decision", - } - self.current_report = ( - f"### {section_titles[latest_section]}\n{latest_content}" - ) - - # Update the final complete report - self._update_final_report() - - def _update_final_report(self): - report_parts = [] - - # Analyst Team Reports - use .get() to handle missing sections - analyst_sections = ["market_report", "sentiment_report", "news_report", "fundamentals_report"] - if any(self.report_sections.get(section) for section in analyst_sections): - report_parts.append("## Analyst Team Reports") - if self.report_sections.get("market_report"): - report_parts.append( - f"### Market Analysis\n{self.report_sections['market_report']}" - ) - if self.report_sections.get("sentiment_report"): - report_parts.append( - f"### Social Sentiment\n{self.report_sections['sentiment_report']}" - ) - if self.report_sections.get("news_report"): - report_parts.append( - f"### News Analysis\n{self.report_sections['news_report']}" - ) - if self.report_sections.get("fundamentals_report"): - report_parts.append( - f"### Fundamentals Analysis\n{self.report_sections['fundamentals_report']}" - ) - - # Research Team Reports - if self.report_sections.get("investment_plan"): - report_parts.append("## Research Team Decision") - report_parts.append(f"{self.report_sections['investment_plan']}") - - # Trading Team Reports - if self.report_sections.get("trader_investment_plan"): - report_parts.append("## Trading Team Plan") - report_parts.append(f"{self.report_sections['trader_investment_plan']}") - - # Portfolio Management Decision - if self.report_sections.get("final_trade_decision"): - report_parts.append("## Portfolio Management Decision") - report_parts.append(f"{self.report_sections['final_trade_decision']}") - - self.final_report = "\n\n".join(report_parts) if report_parts else None - - -message_buffer = MessageBuffer() - - -def create_layout(): - layout = Layout() - layout.split_column( - Layout(name="header", size=3), - Layout(name="main"), - Layout(name="footer", size=3), - ) - layout["main"].split_column( - Layout(name="upper", ratio=3), Layout(name="analysis", ratio=5) - ) - layout["upper"].split_row( - Layout(name="progress", ratio=2), Layout(name="messages", ratio=3) - ) - return layout - - -def format_tokens(n): - """Format token count for display.""" - if n >= 1000: - return f"{n/1000:.1f}k" - return str(n) - - -def update_display(layout, spinner_text=None, stats_handler=None, start_time=None): - # Header with welcome message - layout["header"].update( - Panel( - "[bold green]Welcome to TradingAgents CLI[/bold green]\n" - "[dim]© [Tauric Research](https://github.com/TauricResearch)[/dim]", - title="Welcome to TradingAgents", - border_style="green", - padding=(1, 2), - expand=True, - ) - ) - - # Progress panel showing agent status - progress_table = Table( - show_header=True, - header_style="bold magenta", - show_footer=False, - box=box.SIMPLE_HEAD, # Use simple header with horizontal lines - title=None, # Remove the redundant Progress title - padding=(0, 2), # Add horizontal padding - expand=True, # Make table expand to fill available space - ) - progress_table.add_column("Team", style="cyan", justify="center", width=20) - progress_table.add_column("Agent", style="green", justify="center", width=20) - progress_table.add_column("Status", style="yellow", justify="center", width=20) - - # Group agents by team - filter to only include agents in agent_status - all_teams = { - "Analyst Team": [ - "Market Analyst", - "Social Analyst", - "News Analyst", - "Fundamentals Analyst", - ], - "Research Team": ["Bull Researcher", "Bear Researcher", "Research Manager"], - "Trading Team": ["Trader"], - "Risk Management": ["Aggressive Analyst", "Neutral Analyst", "Conservative Analyst"], - "Portfolio Management": ["Portfolio Manager"], - } - - # Filter teams to only include agents that are in agent_status - teams = {} - for team, agents in all_teams.items(): - active_agents = [a for a in agents if a in message_buffer.agent_status] - if active_agents: - teams[team] = active_agents - - for team, agents in teams.items(): - # Add first agent with team name - first_agent = agents[0] - status = message_buffer.agent_status.get(first_agent, "pending") - if status == "in_progress": - spinner = Spinner( - "dots", text="[blue]in_progress[/blue]", style="bold cyan" - ) - status_cell = spinner - else: - status_color = { - "pending": "yellow", - "completed": "green", - "error": "red", - }.get(status, "white") - status_cell = f"[{status_color}]{status}[/{status_color}]" - progress_table.add_row(team, first_agent, status_cell) - - # Add remaining agents in team - for agent in agents[1:]: - status = message_buffer.agent_status.get(agent, "pending") - if status == "in_progress": - spinner = Spinner( - "dots", text="[blue]in_progress[/blue]", style="bold cyan" - ) - status_cell = spinner - else: - status_color = { - "pending": "yellow", - "completed": "green", - "error": "red", - }.get(status, "white") - status_cell = f"[{status_color}]{status}[/{status_color}]" - progress_table.add_row("", agent, status_cell) - - # Add horizontal line after each team - progress_table.add_row("─" * 20, "─" * 20, "─" * 20, style="dim") - - layout["progress"].update( - Panel(progress_table, title="Progress", border_style="cyan", padding=(1, 2)) - ) - - # Messages panel showing recent messages and tool calls - messages_table = Table( - show_header=True, - header_style="bold magenta", - show_footer=False, - expand=True, # Make table expand to fill available space - box=box.MINIMAL, # Use minimal box style for a lighter look - show_lines=True, # Keep horizontal lines - padding=(0, 1), # Add some padding between columns - ) - messages_table.add_column("Time", style="cyan", width=8, justify="center") - messages_table.add_column("Type", style="green", width=10, justify="center") - messages_table.add_column( - "Content", style="white", no_wrap=False, ratio=1 - ) # Make content column expand - - # Combine tool calls and messages - all_messages = [] - - # Add tool calls - for timestamp, tool_name, args in message_buffer.tool_calls: - formatted_args = format_tool_args(args) - all_messages.append((timestamp, "Tool", f"{tool_name}: {formatted_args}")) - - # Add regular messages - for timestamp, msg_type, content in message_buffer.messages: - content_str = str(content) if content else "" - if len(content_str) > 200: - content_str = content_str[:197] + "..." - all_messages.append((timestamp, msg_type, content_str)) - - # Sort by timestamp descending (newest first) - all_messages.sort(key=lambda x: x[0], reverse=True) - - # Calculate how many messages we can show based on available space - max_messages = 12 - - # Get the first N messages (newest ones) - recent_messages = all_messages[:max_messages] - - # Add messages to table (already in newest-first order) - for timestamp, msg_type, content in recent_messages: - # Format content with word wrapping - wrapped_content = Text(content, overflow="fold") - messages_table.add_row(timestamp, msg_type, wrapped_content) - - layout["messages"].update( - Panel( - messages_table, - title="Messages & Tools", - border_style="blue", - padding=(1, 2), - ) - ) - - # Analysis panel showing current report - if message_buffer.current_report: - layout["analysis"].update( - Panel( - Markdown(message_buffer.current_report), - title="Current Report", - border_style="green", - padding=(1, 2), - ) - ) - else: - layout["analysis"].update( - Panel( - "[italic]Waiting for analysis report...[/italic]", - title="Current Report", - border_style="green", - padding=(1, 2), - ) - ) - - # Footer with statistics - # Agent progress - derived from agent_status dict - agents_completed = sum( - 1 for status in message_buffer.agent_status.values() if status == "completed" - ) - agents_total = len(message_buffer.agent_status) - - # Report progress - based on agent completion (not just content existence) - reports_completed = message_buffer.get_completed_reports_count() - reports_total = len(message_buffer.report_sections) - - # Build stats parts - stats_parts = [f"Agents: {agents_completed}/{agents_total}"] - - # LLM and tool stats from callback handler - if stats_handler: - stats = stats_handler.get_stats() - stats_parts.append(f"LLM: {stats['llm_calls']}") - stats_parts.append(f"Tools: {stats['tool_calls']}") - - # Token display with graceful fallback - if stats["tokens_in"] > 0 or stats["tokens_out"] > 0: - tokens_str = f"Tokens: {format_tokens(stats['tokens_in'])}\u2191 {format_tokens(stats['tokens_out'])}\u2193" - else: - tokens_str = "Tokens: --" - stats_parts.append(tokens_str) - - stats_parts.append(f"Reports: {reports_completed}/{reports_total}") - - # Elapsed time - if start_time: - elapsed = time.time() - start_time - elapsed_str = f"\u23f1 {int(elapsed // 60):02d}:{int(elapsed % 60):02d}" - stats_parts.append(elapsed_str) - - stats_table = Table(show_header=False, box=None, padding=(0, 2), expand=True) - stats_table.add_column("Stats", justify="center") - stats_table.add_row(" | ".join(stats_parts)) - - layout["footer"].update(Panel(stats_table, border_style="grey50")) - - -def _ask_provider_thinking_config(provider: str): - """Ask for provider-specific thinking config. Returns (thinking_level, reasoning_effort).""" - provider_lower = provider.lower() - if provider_lower == "google": - return ask_gemini_thinking_config(), None - elif provider_lower in ("openai", "xai"): - return None, ask_openai_reasoning_effort() - return None, None - - -def get_user_selections(): - """Get all user selections before starting the analysis display.""" - # Display ASCII art welcome message - with open("./cli/static/welcome.txt", "r", encoding="utf-8") as f: - welcome_ascii = f.read() - - # Create welcome box content - welcome_content = f"{welcome_ascii}\n" - welcome_content += "[bold green]TradingAgents: Multi-Agents LLM Financial Trading Framework - CLI[/bold green]\n\n" - welcome_content += "[bold]Workflow Steps:[/bold]\n" - welcome_content += "I. Analyst Team → II. Research Team → III. Trader → IV. Risk Management → V. Portfolio Management\n\n" - welcome_content += ( - "[dim]Built by [Tauric Research](https://github.com/TauricResearch)[/dim]" - ) - - # Create and center the welcome box - welcome_box = Panel( - welcome_content, - border_style="green", - padding=(1, 2), - title="Welcome to TradingAgents", - subtitle="Multi-Agents LLM Financial Trading Framework", - ) - console.print(Align.center(welcome_box)) - console.print() - console.print() # Add vertical space before announcements - - # Fetch and display announcements (silent on failure) - announcements = fetch_announcements() - display_announcements(console, announcements) - - # Create a boxed questionnaire for each step - def create_question_box(title, prompt, default=None): - box_content = f"[bold]{title}[/bold]\n" - box_content += f"[dim]{prompt}[/dim]" - if default: - box_content += f"\n[dim]Default: {default}[/dim]" - return Panel(box_content, border_style="blue", padding=(1, 2)) - - # Step 1: Ticker symbol - console.print( - create_question_box( - "Step 1: Ticker Symbol", "Enter the ticker symbol to analyze", "SPY" - ) - ) - selected_ticker = get_ticker() - - # Step 2: Analysis date - default_date = datetime.datetime.now().strftime("%Y-%m-%d") - console.print( - create_question_box( - "Step 2: Analysis Date", - "Enter the analysis date (YYYY-MM-DD)", - default_date, - ) - ) - analysis_date = get_analysis_date() - - # Step 3: Select analysts - console.print( - create_question_box( - "Step 3: Analysts Team", "Select your LLM analyst agents for the analysis" - ) - ) - selected_analysts = select_analysts() - console.print( - f"[green]Selected analysts:[/green] {', '.join(analyst.value for analyst in selected_analysts)}" - ) - - # Step 4: Research depth - console.print( - create_question_box( - "Step 4: Research Depth", "Select your research depth level" - ) - ) - selected_research_depth = select_research_depth() - - # Step 5: Quick-thinking provider + model - console.print( - create_question_box( - "Step 5: Quick-Thinking Setup", - "Provider and model for analysts & risk debaters (fast, high volume)" - ) - ) - quick_provider, quick_backend_url = select_llm_provider() - selected_shallow_thinker = select_shallow_thinking_agent(quick_provider) - quick_thinking_level, quick_reasoning_effort = _ask_provider_thinking_config(quick_provider) - - # Step 6: Mid-thinking provider + model - console.print( - create_question_box( - "Step 6: Mid-Thinking Setup", - "Provider and model for researchers & trader (reasoning, argument formation)" - ) - ) - mid_provider, mid_backend_url = select_llm_provider() - selected_mid_thinker = select_mid_thinking_agent(mid_provider) - mid_thinking_level, mid_reasoning_effort = _ask_provider_thinking_config(mid_provider) - - # Step 7: Deep-thinking provider + model - console.print( - create_question_box( - "Step 7: Deep-Thinking Setup", - "Provider and model for investment judge & risk manager (final decisions)" - ) - ) - deep_provider, deep_backend_url = select_llm_provider() - selected_deep_thinker = select_deep_thinking_agent(deep_provider) - deep_thinking_level, deep_reasoning_effort = _ask_provider_thinking_config(deep_provider) - - return { - "ticker": selected_ticker, - "analysis_date": analysis_date, - "analysts": selected_analysts, - "research_depth": selected_research_depth, - # Quick - "quick_provider": quick_provider.lower(), - "quick_backend_url": quick_backend_url, - "shallow_thinker": selected_shallow_thinker, - "quick_thinking_level": quick_thinking_level, - "quick_reasoning_effort": quick_reasoning_effort, - # Mid - "mid_provider": mid_provider.lower(), - "mid_backend_url": mid_backend_url, - "mid_thinker": selected_mid_thinker, - "mid_thinking_level": mid_thinking_level, - "mid_reasoning_effort": mid_reasoning_effort, - # Deep - "deep_provider": deep_provider.lower(), - "deep_backend_url": deep_backend_url, - "deep_thinker": selected_deep_thinker, - "deep_thinking_level": deep_thinking_level, - "deep_reasoning_effort": deep_reasoning_effort, - } - - -def save_report_to_disk(final_state, ticker: str, save_path: Path): - """Save complete analysis report to disk with organized subfolders.""" - save_path.mkdir(parents=True, exist_ok=True) - sections = [] - - # 1. Analysts - analysts_dir = save_path / "1_analysts" - analyst_parts = [] - if final_state.get("market_report"): - analysts_dir.mkdir(exist_ok=True) - (analysts_dir / "market.md").write_text(final_state["market_report"]) - analyst_parts.append(("Market Analyst", final_state["market_report"])) - if final_state.get("sentiment_report"): - analysts_dir.mkdir(exist_ok=True) - (analysts_dir / "sentiment.md").write_text(final_state["sentiment_report"]) - analyst_parts.append(("Social Analyst", final_state["sentiment_report"])) - if final_state.get("news_report"): - analysts_dir.mkdir(exist_ok=True) - (analysts_dir / "news.md").write_text(final_state["news_report"]) - analyst_parts.append(("News Analyst", final_state["news_report"])) - if final_state.get("fundamentals_report"): - analysts_dir.mkdir(exist_ok=True) - (analysts_dir / "fundamentals.md").write_text(final_state["fundamentals_report"]) - analyst_parts.append(("Fundamentals Analyst", final_state["fundamentals_report"])) - if analyst_parts: - content = "\n\n".join(f"### {name}\n{text}" for name, text in analyst_parts) - sections.append(f"## I. Analyst Team Reports\n\n{content}") - - # 2. Research - if final_state.get("investment_debate_state"): - research_dir = save_path / "2_research" - debate = final_state["investment_debate_state"] - research_parts = [] - if debate.get("bull_history"): - research_dir.mkdir(exist_ok=True) - (research_dir / "bull.md").write_text(debate["bull_history"]) - research_parts.append(("Bull Researcher", debate["bull_history"])) - if debate.get("bear_history"): - research_dir.mkdir(exist_ok=True) - (research_dir / "bear.md").write_text(debate["bear_history"]) - research_parts.append(("Bear Researcher", debate["bear_history"])) - if debate.get("judge_decision"): - research_dir.mkdir(exist_ok=True) - (research_dir / "manager.md").write_text(debate["judge_decision"]) - research_parts.append(("Research Manager", debate["judge_decision"])) - if research_parts: - content = "\n\n".join(f"### {name}\n{text}" for name, text in research_parts) - sections.append(f"## II. Research Team Decision\n\n{content}") - - # 3. Trading - if final_state.get("trader_investment_plan"): - trading_dir = save_path / "3_trading" - trading_dir.mkdir(exist_ok=True) - (trading_dir / "trader.md").write_text(final_state["trader_investment_plan"]) - sections.append(f"## III. Trading Team Plan\n\n### Trader\n{final_state['trader_investment_plan']}") - - # 4. Risk Management - if final_state.get("risk_debate_state"): - risk_dir = save_path / "4_risk" - risk = final_state["risk_debate_state"] - risk_parts = [] - if risk.get("aggressive_history"): - risk_dir.mkdir(exist_ok=True) - (risk_dir / "aggressive.md").write_text(risk["aggressive_history"]) - risk_parts.append(("Aggressive Analyst", risk["aggressive_history"])) - if risk.get("conservative_history"): - risk_dir.mkdir(exist_ok=True) - (risk_dir / "conservative.md").write_text(risk["conservative_history"]) - risk_parts.append(("Conservative Analyst", risk["conservative_history"])) - if risk.get("neutral_history"): - risk_dir.mkdir(exist_ok=True) - (risk_dir / "neutral.md").write_text(risk["neutral_history"]) - risk_parts.append(("Neutral Analyst", risk["neutral_history"])) - if risk_parts: - content = "\n\n".join(f"### {name}\n{text}" for name, text in risk_parts) - sections.append(f"## IV. Risk Management Team Decision\n\n{content}") - - # 5. Portfolio Manager - if risk.get("judge_decision"): - portfolio_dir = save_path / "5_portfolio" - portfolio_dir.mkdir(exist_ok=True) - (portfolio_dir / "decision.md").write_text(risk["judge_decision"]) - sections.append(f"## V. Portfolio Manager Decision\n\n### Portfolio Manager\n{risk['judge_decision']}") - - # Write consolidated report - header = f"# Trading Analysis Report: {ticker}\n\nGenerated: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n" - (save_path / "complete_report.md").write_text(header + "\n\n".join(sections)) - return save_path / "complete_report.md" - - -def display_complete_report(final_state): - """Display the complete analysis report sequentially (avoids truncation).""" - console.print() - console.print(Rule("Complete Analysis Report", style="bold green")) - - # I. Analyst Team Reports - analysts = [] - if final_state.get("market_report"): - analysts.append(("Market Analyst", final_state["market_report"])) - if final_state.get("sentiment_report"): - analysts.append(("Social Analyst", final_state["sentiment_report"])) - if final_state.get("news_report"): - analysts.append(("News Analyst", final_state["news_report"])) - if final_state.get("fundamentals_report"): - analysts.append(("Fundamentals Analyst", final_state["fundamentals_report"])) - if analysts: - console.print(Panel("[bold]I. Analyst Team Reports[/bold]", border_style="cyan")) - for title, content in analysts: - console.print(Panel(Markdown(content), title=title, border_style="blue", padding=(1, 2))) - - # II. Research Team Reports - if final_state.get("investment_debate_state"): - debate = final_state["investment_debate_state"] - research = [] - if debate.get("bull_history"): - research.append(("Bull Researcher", debate["bull_history"])) - if debate.get("bear_history"): - research.append(("Bear Researcher", debate["bear_history"])) - if debate.get("judge_decision"): - research.append(("Research Manager", debate["judge_decision"])) - if research: - console.print(Panel("[bold]II. Research Team Decision[/bold]", border_style="magenta")) - for title, content in research: - console.print(Panel(Markdown(content), title=title, border_style="blue", padding=(1, 2))) - - # III. Trading Team - if final_state.get("trader_investment_plan"): - console.print(Panel("[bold]III. Trading Team Plan[/bold]", border_style="yellow")) - console.print(Panel(Markdown(final_state["trader_investment_plan"]), title="Trader", border_style="blue", padding=(1, 2))) - - # IV. Risk Management Team - if final_state.get("risk_debate_state"): - risk = final_state["risk_debate_state"] - risk_reports = [] - if risk.get("aggressive_history"): - risk_reports.append(("Aggressive Analyst", risk["aggressive_history"])) - if risk.get("conservative_history"): - risk_reports.append(("Conservative Analyst", risk["conservative_history"])) - if risk.get("neutral_history"): - risk_reports.append(("Neutral Analyst", risk["neutral_history"])) - if risk_reports: - console.print(Panel("[bold]IV. Risk Management Team Decision[/bold]", border_style="red")) - for title, content in risk_reports: - console.print(Panel(Markdown(content), title=title, border_style="blue", padding=(1, 2))) - - # V. Portfolio Manager Decision - if risk.get("judge_decision"): - console.print(Panel("[bold]V. Portfolio Manager Decision[/bold]", border_style="green")) - console.print(Panel(Markdown(risk["judge_decision"]), title="Portfolio Manager", border_style="blue", padding=(1, 2))) - - -def update_research_team_status(status): - """Update status for research team members (not Trader).""" - research_team = ["Bull Researcher", "Bear Researcher", "Research Manager"] - for agent in research_team: - message_buffer.update_agent_status(agent, status) - - -# Ordered list of analysts for status transitions -ANALYST_ORDER = ["market", "social", "news", "fundamentals"] -ANALYST_AGENT_NAMES = { - "market": "Market Analyst", - "social": "Social Analyst", - "news": "News Analyst", - "fundamentals": "Fundamentals Analyst", -} -ANALYST_REPORT_MAP = { - "market": "market_report", - "social": "sentiment_report", - "news": "news_report", - "fundamentals": "fundamentals_report", -} - - -def update_analyst_statuses(message_buffer, chunk): - """Update all analyst statuses based on current report state. - - Logic: - - Analysts with reports = completed - - First analyst without report = in_progress - - Remaining analysts without reports = pending - - When all analysts done, set Bull Researcher to in_progress - """ - selected = message_buffer.selected_analysts - found_active = False - - for analyst_key in ANALYST_ORDER: - if analyst_key not in selected: - continue - - agent_name = ANALYST_AGENT_NAMES[analyst_key] - report_key = ANALYST_REPORT_MAP[analyst_key] - has_report = bool(chunk.get(report_key)) - - if has_report: - message_buffer.update_agent_status(agent_name, "completed") - message_buffer.update_report_section(report_key, chunk[report_key]) - elif not found_active: - message_buffer.update_agent_status(agent_name, "in_progress") - found_active = True - else: - message_buffer.update_agent_status(agent_name, "pending") - - # When all analysts complete, transition research team to in_progress - if not found_active and selected: - if message_buffer.agent_status.get("Bull Researcher") == "pending": - message_buffer.update_agent_status("Bull Researcher", "in_progress") - -def extract_content_string(content): - """Extract string content from various message formats. - Returns None if no meaningful text content is found. - """ - import ast - - def is_empty(val): - """Check if value is empty using Python's truthiness.""" - if val is None or val == '': - return True - if isinstance(val, str): - s = val.strip() - if not s: - return True - try: - return not bool(ast.literal_eval(s)) - except (ValueError, SyntaxError): - return False # Can't parse = real text - return not bool(val) - - if is_empty(content): - return None - - if isinstance(content, str): - return content.strip() - - if isinstance(content, dict): - text = content.get('text', '') - return text.strip() if not is_empty(text) else None - - if isinstance(content, list): - text_parts = [ - item.get('text', '').strip() if isinstance(item, dict) and item.get('type') == 'text' - else (item.strip() if isinstance(item, str) else '') - for item in content - ] - result = ' '.join(t for t in text_parts if t and not is_empty(t)) - return result if result else None - - return str(content).strip() if not is_empty(content) else None - - -def classify_message_type(message) -> tuple[str, str | None]: - """Classify LangChain message into display type and extract content. - - Returns: - (type, content) - type is one of: User, Agent, Data, Control - - content is extracted string or None - """ - from langchain_core.messages import AIMessage, HumanMessage, ToolMessage - - content = extract_content_string(getattr(message, 'content', None)) - - if isinstance(message, HumanMessage): - if content and content.strip() == "Continue": - return ("Control", content) - return ("User", content) - - if isinstance(message, ToolMessage): - return ("Data", content) - - if isinstance(message, AIMessage): - return ("Agent", content) - - # Fallback for unknown types - return ("System", content) - - -def format_tool_args(args, max_length=80) -> str: - """Format tool arguments for terminal display.""" - result = str(args) - if len(result) > max_length: - return result[:max_length - 3] + "..." - return result - -def run_analysis(): - # First get all user selections - selections = get_user_selections() - - # Create config with selected research depth - config = DEFAULT_CONFIG.copy() - config["max_debate_rounds"] = selections["research_depth"] - config["max_risk_discuss_rounds"] = selections["research_depth"] - # Per-role LLM configuration - config["quick_think_llm"] = selections["shallow_thinker"] - config["quick_think_llm_provider"] = selections["quick_provider"] - config["quick_think_backend_url"] = selections["quick_backend_url"] - config["quick_think_google_thinking_level"] = selections.get("quick_thinking_level") - config["quick_think_openai_reasoning_effort"] = selections.get("quick_reasoning_effort") - config["mid_think_llm"] = selections["mid_thinker"] - config["mid_think_llm_provider"] = selections["mid_provider"] - config["mid_think_backend_url"] = selections["mid_backend_url"] - config["mid_think_google_thinking_level"] = selections.get("mid_thinking_level") - config["mid_think_openai_reasoning_effort"] = selections.get("mid_reasoning_effort") - config["deep_think_llm"] = selections["deep_thinker"] - config["deep_think_llm_provider"] = selections["deep_provider"] - config["deep_think_backend_url"] = selections["deep_backend_url"] - config["deep_think_google_thinking_level"] = selections.get("deep_thinking_level") - config["deep_think_openai_reasoning_effort"] = selections.get("deep_reasoning_effort") - # Keep shared llm_provider/backend_url as a fallback (use quick as default) - config["llm_provider"] = selections["quick_provider"] - config["backend_url"] = selections["quick_backend_url"] - - # Create stats callback handler for tracking LLM/tool calls - stats_handler = StatsCallbackHandler() - - # Normalize analyst selection to predefined order (selection is a 'set', order is fixed) - selected_set = {analyst.value for analyst in selections["analysts"]} - selected_analyst_keys = [a for a in ANALYST_ORDER if a in selected_set] - - # Initialize the graph with callbacks bound to LLMs - graph = TradingAgentsGraph( - selected_analyst_keys, - config=config, - debug=True, - callbacks=[stats_handler], - ) - - # Initialize message buffer with selected analysts - message_buffer.init_for_analysis(selected_analyst_keys) - - # Track start time for elapsed display - start_time = time.time() - - # Create result directory - results_dir = Path(config["results_dir"]) / selections["ticker"] / selections["analysis_date"] - results_dir.mkdir(parents=True, exist_ok=True) - report_dir = results_dir / "reports" - report_dir.mkdir(parents=True, exist_ok=True) - log_file = results_dir / "message_tool.log" - log_file.touch(exist_ok=True) - - def save_message_decorator(obj, func_name): - func = getattr(obj, func_name) - @wraps(func) - def wrapper(*args, **kwargs): - func(*args, **kwargs) - timestamp, message_type, content = obj.messages[-1] - content = content.replace("\n", " ") # Replace newlines with spaces - with open(log_file, "a", encoding="utf-8") as f: - f.write(f"{timestamp} [{message_type}] {content}\n") - return wrapper - - def save_tool_call_decorator(obj, func_name): - func = getattr(obj, func_name) - @wraps(func) - def wrapper(*args, **kwargs): - func(*args, **kwargs) - timestamp, tool_name, args = obj.tool_calls[-1] - args_str = ", ".join(f"{k}={v}" for k, v in args.items()) - with open(log_file, "a", encoding="utf-8") as f: - f.write(f"{timestamp} [Tool Call] {tool_name}({args_str})\n") - return wrapper - - def save_report_section_decorator(obj, func_name): - func = getattr(obj, func_name) - @wraps(func) - def wrapper(section_name, content): - func(section_name, content) - if section_name in obj.report_sections and obj.report_sections[section_name] is not None: - content = obj.report_sections[section_name] - if content: - file_name = f"{section_name}.md" - with open(report_dir / file_name, "w", encoding="utf-8") as f: - f.write(content) - return wrapper - - message_buffer.add_message = save_message_decorator(message_buffer, "add_message") - message_buffer.add_tool_call = save_tool_call_decorator(message_buffer, "add_tool_call") - message_buffer.update_report_section = save_report_section_decorator(message_buffer, "update_report_section") - - # Now start the display layout - layout = create_layout() - - with Live(layout, refresh_per_second=4) as live: - # Initial display - update_display(layout, stats_handler=stats_handler, start_time=start_time) - - # Add initial messages - message_buffer.add_message("System", f"Selected ticker: {selections['ticker']}") - message_buffer.add_message( - "System", f"Analysis date: {selections['analysis_date']}" - ) - message_buffer.add_message( - "System", - f"Selected analysts: {', '.join(analyst.value for analyst in selections['analysts'])}", - ) - update_display(layout, stats_handler=stats_handler, start_time=start_time) - - # Update agent status to in_progress for the first analyst - first_analyst = f"{selections['analysts'][0].value.capitalize()} Analyst" - message_buffer.update_agent_status(first_analyst, "in_progress") - update_display(layout, stats_handler=stats_handler, start_time=start_time) - - # Create spinner text - spinner_text = ( - f"Analyzing {selections['ticker']} on {selections['analysis_date']}..." - ) - update_display(layout, spinner_text, stats_handler=stats_handler, start_time=start_time) - - # Initialize state and get graph args with callbacks - init_agent_state = graph.propagator.create_initial_state( - selections["ticker"], selections["analysis_date"] - ) - # Pass callbacks to graph config for tool execution tracking - # (LLM tracking is handled separately via LLM constructor) - args = graph.propagator.get_graph_args(callbacks=[stats_handler]) - - # Stream the analysis - trace = [] - for chunk in graph.graph.stream(init_agent_state, **args): - # Process messages if present (skip duplicates via message ID) - if len(chunk["messages"]) > 0: - last_message = chunk["messages"][-1] - msg_id = getattr(last_message, "id", None) - - if msg_id != message_buffer._last_message_id: - message_buffer._last_message_id = msg_id - - # Add message to buffer - msg_type, content = classify_message_type(last_message) - if content and content.strip(): - message_buffer.add_message(msg_type, content) - - # Handle tool calls - if hasattr(last_message, "tool_calls") and last_message.tool_calls: - for tool_call in last_message.tool_calls: - if isinstance(tool_call, dict): - message_buffer.add_tool_call( - tool_call["name"], tool_call["args"] - ) - else: - message_buffer.add_tool_call(tool_call.name, tool_call.args) - - # Update analyst statuses based on report state (runs on every chunk) - update_analyst_statuses(message_buffer, chunk) - - # Research Team - Handle Investment Debate State - if chunk.get("investment_debate_state"): - debate_state = chunk["investment_debate_state"] - bull_hist = debate_state.get("bull_history", "").strip() - bear_hist = debate_state.get("bear_history", "").strip() - judge = debate_state.get("judge_decision", "").strip() - - # Only update status when there's actual content - if bull_hist or bear_hist: - update_research_team_status("in_progress") - if bull_hist: - message_buffer.update_report_section( - "investment_plan", f"### Bull Researcher Analysis\n{bull_hist}" - ) - if bear_hist: - message_buffer.update_report_section( - "investment_plan", f"### Bear Researcher Analysis\n{bear_hist}" - ) - if judge: - message_buffer.update_report_section( - "investment_plan", f"### Research Manager Decision\n{judge}" - ) - update_research_team_status("completed") - message_buffer.update_agent_status("Trader", "in_progress") - - # Trading Team - if chunk.get("trader_investment_plan"): - message_buffer.update_report_section( - "trader_investment_plan", chunk["trader_investment_plan"] - ) - if message_buffer.agent_status.get("Trader") != "completed": - message_buffer.update_agent_status("Trader", "completed") - message_buffer.update_agent_status("Aggressive Analyst", "in_progress") - - # Risk Management Team - Handle Risk Debate State - if chunk.get("risk_debate_state"): - risk_state = chunk["risk_debate_state"] - agg_hist = risk_state.get("aggressive_history", "").strip() - con_hist = risk_state.get("conservative_history", "").strip() - neu_hist = risk_state.get("neutral_history", "").strip() - judge = risk_state.get("judge_decision", "").strip() - - if agg_hist: - if message_buffer.agent_status.get("Aggressive Analyst") != "completed": - message_buffer.update_agent_status("Aggressive Analyst", "in_progress") - message_buffer.update_report_section( - "final_trade_decision", f"### Aggressive Analyst Analysis\n{agg_hist}" - ) - if con_hist: - if message_buffer.agent_status.get("Conservative Analyst") != "completed": - message_buffer.update_agent_status("Conservative Analyst", "in_progress") - message_buffer.update_report_section( - "final_trade_decision", f"### Conservative Analyst Analysis\n{con_hist}" - ) - if neu_hist: - if message_buffer.agent_status.get("Neutral Analyst") != "completed": - message_buffer.update_agent_status("Neutral Analyst", "in_progress") - message_buffer.update_report_section( - "final_trade_decision", f"### Neutral Analyst Analysis\n{neu_hist}" - ) - if judge: - if message_buffer.agent_status.get("Portfolio Manager") != "completed": - message_buffer.update_agent_status("Portfolio Manager", "in_progress") - message_buffer.update_report_section( - "final_trade_decision", f"### Portfolio Manager Decision\n{judge}" - ) - message_buffer.update_agent_status("Aggressive Analyst", "completed") - message_buffer.update_agent_status("Conservative Analyst", "completed") - message_buffer.update_agent_status("Neutral Analyst", "completed") - message_buffer.update_agent_status("Portfolio Manager", "completed") - - # Update the display - update_display(layout, stats_handler=stats_handler, start_time=start_time) - - trace.append(chunk) - - # Get final state and decision - final_state = trace[-1] - decision = graph.process_signal(final_state["final_trade_decision"]) - - # Update all agent statuses to completed - for agent in message_buffer.agent_status: - message_buffer.update_agent_status(agent, "completed") - - message_buffer.add_message( - "System", f"Completed analysis for {selections['analysis_date']}" - ) - - # Update final report sections - for section in message_buffer.report_sections.keys(): - if section in final_state: - message_buffer.update_report_section(section, final_state[section]) - - update_display(layout, stats_handler=stats_handler, start_time=start_time) - - # Post-analysis prompts (outside Live context for clean interaction) - console.print("\n[bold cyan]Analysis Complete![/bold cyan]\n") - - # Prompt to save report - save_choice = typer.prompt("Save report?", default="Y").strip().upper() - if save_choice in ("Y", "YES", ""): - timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") - default_path = Path.cwd() / "reports" / f"{selections['ticker']}_{timestamp}" - save_path_str = typer.prompt( - "Save path (press Enter for default)", - default=str(default_path) - ).strip() - save_path = Path(save_path_str) - try: - report_file = save_report_to_disk(final_state, selections["ticker"], save_path) - console.print(f"\n[green]✓ Report saved to:[/green] {save_path.resolve()}") - console.print(f" [dim]Complete report:[/dim] {report_file.name}") - except Exception as e: - console.print(f"[red]Error saving report: {e}[/red]") - - # Prompt to display full report - display_choice = typer.prompt("\nDisplay full report on screen?", default="Y").strip().upper() - if display_choice in ("Y", "YES", ""): - display_complete_report(final_state) - - -def run_scan(): - console.print(Panel("[bold green]Global Macro Scanner[/bold green]", border_style="green")) - default_date = datetime.datetime.now().strftime("%Y-%m-%d") - scan_date = typer.prompt("Scan date (YYYY-MM-DD)", default=default_date) - console.print(f"[cyan]Scanning market data for {scan_date}...[/cyan]") - - # Prepare save directory - save_dir = Path("results/macro_scan") / scan_date - save_dir.mkdir(parents=True, exist_ok=True) - - # Call scanner tools - console.print("[bold]1. Market Movers[/bold]") - movers = get_market_movers("day_gainers") - if not (movers.startswith("Error") or movers.startswith("No data")): - (save_dir / "market_movers.txt").write_text(movers) - console.print(movers[:500] + "..." if len(movers) > 500 else movers) - - console.print("[bold]2. Market Indices[/bold]") - indices = get_market_indices() - if not (indices.startswith("Error") or indices.startswith("No data")): - (save_dir / "market_indices.txt").write_text(indices) - console.print(indices[:500] + "..." if len(indices) > 500 else indices) - - console.print("[bold]3. Sector Performance[/bold]") - sectors = get_sector_performance() - if not (sectors.startswith("Error") or sectors.startswith("No data")): - (save_dir / "sector_performance.txt").write_text(sectors) - console.print(sectors[:500] + "..." if len(sectors) > 500 else sectors) - - console.print("[bold]4. Industry Performance (Technology)[/bold]") - industry = get_industry_performance("technology") - if not (industry.startswith("Error") or industry.startswith("No data")): - (save_dir / "industry_performance.txt").write_text(industry) - console.print(industry[:500] + "..." if len(industry) > 500 else industry) - - console.print("[bold]5. Topic News (Market)[/bold]") - news = get_topic_news("market") - if not (news.startswith("Error") or news.startswith("No data")): - (save_dir / "topic_news.txt").write_text(news) - console.print(news[:500] + "..." if len(news) > 500 else news) - - console.print(f"[green]Results saved to {save_dir}[/green]") - - -@app.command() -def analyze(): - run_analysis() - - -@app.command() -def scan(): - run_scan() - - -if __name__ == "__main__": - app() diff --git a/plans/execution_plan_data_layer_fix_and_test.md b/plans/execution_plan_data_layer_fix_and_test.md deleted file mode 100644 index c5ac186f..00000000 --- a/plans/execution_plan_data_layer_fix_and_test.md +++ /dev/null @@ -1,82 +0,0 @@ -# Data Layer Fix and Test Plan for Global Macro Analyzer - -## Current State Assessment - -- ✅ pyproject.toml configured correctly -- ✅ Removed stray scanner_tools.py files outside tradingagents/ -- ✅ yfinance_scanner.py implements all required functions -- ✅ alpha_vantage_scanner.py implements fallback get_market_movers_alpha_vantage correctly -- ✅ scanner_tools.py wrappers properly use route_to_vendor for all scanner methods -- ✅ default_config.py updated with scanner_data vendor configuration -- ✅ All scanner tools import successfully without runtime errors - -## Outstanding Issues - -- CLI scan command not yet implemented in cli/main.py -- Scanner graph components (MacroScannerGraph) not yet created -- No end-to-end testing of the data layer functionality - -## Fix Plan - -### 1. Implement Scanner Graph Components - -Create the following files in tradingagents/graph/: - -- scanner_setup.py: Graph setup logic for scanner components -- scanner_conditional_logic.py: Conditional logic for scanner graph flow -- scanner_graph.py: Main MacroScannerGraph class - -### 2. Add Scan Command to CLI - -Modify cli/main.py to include: - -- @app.command() def scan(): entry point -- Date prompt (default: today) -- LLM provider config prompt (reuse existing helpers) -- MacroScannerGraph instantiation and scan() method call -- Rich panel display for results -- Report saving to results/macro_scan/{date}/ directory - -### 3. Create MacroScannerGraph - -Implement the scanner graph that: - -- Runs parallel Phase 1 scanners (geopolitical, market movers, sectors) -- Coordinates Phase 2 industry deep dive -- Produces Phase 3 macro synthesis output -- Uses ScannerState for state management - -### 4. End-to-End Testing - -Execute the scan command and verify: - -- Rich panels display correctly for each report section -- Top-10 stock watchlist is generated and displayed -- Reports are saved to results/macro_scan/{date}/ directory -- No import or runtime errors occur - -## Implementation Steps - -1. [ ] Create scanner graph components (scanner_setup.py, scanner_conditional_logic.py, scanner_graph.py) -2. [ ] Add scan command to cli/main.py with proper argument handling -3. [ ] Implement MacroScannerGraph with proper node/edge connections -4. [ ] Test scan command functionality -5. [ ] Verify output formatting and file generation -6. [ ] Document test results and any issues found - -## Verification Criteria - -- ✅ All scanner tools can be imported and used -- ✅ CLI scan command executes without errors -- ✅ Rich panels display market movers, indices, sector performance, and news -- ✅ Top-10 stock watchlist is generated and displayed -- ✅ Reports saved to results/macro_scan/{date}/ directory -- ✅ No runtime exceptions or import errors - -## Contingency - -- If errors occur during scan execution, check: - - Vendor routing configuration in default_config.py - - Function implementations in yfinance_scanner.py and alpha_vantage_scanner.py - - Graph node/edge connections in scanner graph components - - Rich panel formatting and output generation logic diff --git a/plans/execution_plan_data_layer_test.md b/plans/execution_plan_data_layer_test.md deleted file mode 100644 index 234d0840..00000000 --- a/plans/execution_plan_data_layer_test.md +++ /dev/null @@ -1,49 +0,0 @@ -# Data Layer Fix and Test Plan - -## Goal - -Verify and test the data layer for the Global Macro Analyzer implementation. - -## Prerequisites - -- Python environment with dependencies installed -- yfinance and alpha_vantage configured - -## Steps - -1. Import and test scanner tools individually -2. Run CLI scan command -3. Validate output -4. Document results - -## Testing Scanner Tools - -- Test get_market_movers -- Test get_market_indices -- Test get_sector_performance -- Test get_industry_performance -- Test get_topic_news - -## Running CLI Scan - -- Command: python -m tradingagents scan --date 2026-03-14 -- Expected output: Rich panels with market movers, indices, sector performance, news, and top-10 watchlist - -## Expected Results - -- No import errors -- Successful execution without exceptions -- Output files generated under results/ -- Top-10 stock watchlist displayed - -## Contingency - -- If errors occur, check import paths and configuration -- Verify default_config.py scanner_data setting is correct -- Ensure vendor routing works correctly - -## Next Steps - -- Address any failures -- Refine output formatting -- Add additional test cases diff --git a/results/macro_scan/2026-03-15/industry_performance.txt b/results/macro_scan/2026-03-15/industry_performance.txt deleted file mode 100644 index e15947b5..00000000 --- a/results/macro_scan/2026-03-15/industry_performance.txt +++ /dev/null @@ -1,25 +0,0 @@ -# Industry Performance: Technology -# Data retrieved on: 2026-03-15 11:17:42 - -| Company | Symbol | Industry | Market Cap | Change % | -|---------|--------|----------|------------|----------| -| NVIDIA Corporation | N/A | N/A | N/A | N/A | -| Apple Inc. | N/A | N/A | N/A | N/A | -| Microsoft Corporation | N/A | N/A | N/A | N/A | -| Broadcom Inc. | N/A | N/A | N/A | N/A | -| Micron Technology, Inc. | N/A | N/A | N/A | N/A | -| Oracle Corporation | N/A | N/A | N/A | N/A | -| Palantir Technologies Inc. | N/A | N/A | N/A | N/A | -| Advanced Micro Devices, Inc. | N/A | N/A | N/A | N/A | -| Cisco Systems, Inc. | N/A | N/A | N/A | N/A | -| Applied Materials, Inc. | N/A | N/A | N/A | N/A | -| Lam Research Corporation | N/A | N/A | N/A | N/A | -| International Business Machine | N/A | N/A | N/A | N/A | -| Intel Corporation | N/A | N/A | N/A | N/A | -| KLA Corporation | N/A | N/A | N/A | N/A | -| Salesforce, Inc. | N/A | N/A | N/A | N/A | -| Texas Instruments Incorporated | N/A | N/A | N/A | N/A | -| Arista Networks, Inc. | N/A | N/A | N/A | N/A | -| Amphenol Corporation | N/A | N/A | N/A | N/A | -| Shopify Inc. | N/A | N/A | N/A | N/A | -| Uber Technologies, Inc. | N/A | N/A | N/A | N/A | diff --git a/results/macro_scan/2026-03-15/market_indices.txt b/results/macro_scan/2026-03-15/market_indices.txt deleted file mode 100644 index 05b738d7..00000000 --- a/results/macro_scan/2026-03-15/market_indices.txt +++ /dev/null @@ -1,10 +0,0 @@ -# Major Market Indices -# Data retrieved on: 2026-03-15 11:17:38 - -| Index | Current Price | Change | Change % | 52W High | 52W Low | -|-------|---------------|--------|----------|----------|----------| -| S&P 500 | 6632.19 | -40.43 | -0.61% | 7002.28 | 4835.04 | -| Dow Jones | 46558.47 | -119.38 | -0.26% | 50512.79 | 36611.78 | -| NASDAQ | 22105.36 | -206.62 | -0.93% | 24019.99 | 14784.03 | -| VIX (Volatility Index) | 27.19 | -0.10 | -0.37% | 60.13 | 13.38 | -| Russell 2000 | 2480.05 | -8.94 | -0.36% | 2735.10 | 1732.99 | diff --git a/results/macro_scan/2026-03-15/market_movers.txt b/results/macro_scan/2026-03-15/market_movers.txt deleted file mode 100644 index 3c0e8df2..00000000 --- a/results/macro_scan/2026-03-15/market_movers.txt +++ /dev/null @@ -1,20 +0,0 @@ -# Market Movers: Day Gainers -# Data retrieved on: 2026-03-15 11:17:38 - -| Symbol | Name | Price | Change % | Volume | Market Cap | -|--------|------|-------|----------|--------|------------| -| NP | Neptune Insurance Holdings Inc | $21.87 | 20.23% | 924,853 | $3,021,417,984 | -| VEON | VEON Ltd. | $50.60 | 14.20% | 687,398 | $3,491,177,216 | -| KLAR | Klarna Group plc | $15.91 | 8.82% | 8,979,495 | $6,006,150,656 | -| KYIV | Kyivstar Group Ltd. | $11.07 | 8.53% | 2,498,383 | $2,555,660,032 | -| GLXY | Galaxy Digital Inc. | $22.35 | 8.34% | 7,046,744 | $8,730,140,672 | -| BLLN | BillionToOne, Inc. | $69.11 | 7.93% | 230,655 | $3,165,566,720 | -| IBRX | ImmunityBio, Inc. | $8.39 | 7.29% | 30,384,030 | $8,625,855,488 | -| SNDK | Sandisk Corporation | $661.62 | 6.92% | 18,684,442 | $97,655,758,848 | -| SSL | Sasol Ltd. | $11.31 | 6.70% | 5,267,106 | $7,210,551,296 | -| SEDG | SolarEdge Technologies, Inc. | $37.44 | 6.39% | 1,971,961 | $2,260,113,920 | -| MARA | MARA Holdings, Inc. | $9.32 | 6.39% | 73,011,343 | $3,543,786,496 | -| MUR | Murphy Oil Corporation | $36.81 | 6.02% | 5,770,011 | $5,257,585,664 | -| ADPT | Adaptive Biotechnologies Corpo | $13.17 | 5.78% | 3,892,105 | $2,027,937,280 | -| NIO | NIO Inc. | $5.86 | 5.59% | 57,679,174 | $14,817,943,552 | -| CRDO | Credo Technology Group Holding | $117.69 | 5.49% | 4,460,224 | $21,707,913,216 | diff --git a/results/macro_scan/2026-03-15/sector_performance.txt b/results/macro_scan/2026-03-15/sector_performance.txt deleted file mode 100644 index baa26382..00000000 --- a/results/macro_scan/2026-03-15/sector_performance.txt +++ /dev/null @@ -1,16 +0,0 @@ -# Sector Performance Overview -# Data retrieved on: 2026-03-15 11:17:40 - -| Sector | 1-Day % | 1-Week % | 1-Month % | YTD % | -|--------|---------|----------|-----------|-------| -| Communication Services | N/A | N/A | N/A | N/A | -| Consumer Cyclical | N/A | N/A | N/A | N/A | -| Consumer Defensive | N/A | N/A | N/A | N/A | -| Energy | N/A | N/A | N/A | N/A | -| Financial Services | N/A | N/A | N/A | N/A | -| Healthcare | N/A | N/A | N/A | N/A | -| Industrials | N/A | N/A | N/A | N/A | -| Basic Materials | N/A | N/A | N/A | N/A | -| Real Estate | N/A | N/A | N/A | N/A | -| Technology | N/A | N/A | N/A | N/A | -| Utilities | N/A | N/A | N/A | N/A | diff --git a/results/macro_scan/2026-03-15/topic_news.txt b/results/macro_scan/2026-03-15/topic_news.txt deleted file mode 100644 index 2754981b..00000000 --- a/results/macro_scan/2026-03-15/topic_news.txt +++ /dev/null @@ -1,33 +0,0 @@ -# News for Topic: market -# Data retrieved on: 2026-03-15 11:17:42 - -### Opinion: A Stock Market Crash Is Much More Likely Now Than It Was 2 Months Ago (source: Motley Fool) -Link: https://finance.yahoo.com/m/f5bf5eda-ecb7-3918-9b7a-0b4ebce070cf/opinion%3A-a-stock-market-crash.html - -### UBS: AI investment is ‘lone buffer’ for emerging markets as energy costs soar (source: Investing.com) -Link: https://finance.yahoo.com/news/ubs-ai-investment-lone-buffer-021705455.html - -### The Stock Market May Be Shifting From Risky Tech Stocks to Safer Sectors. Here Are 3 Stocks to Buy Before They Soar. (source: Motley Fool) -Link: https://finance.yahoo.com/m/36037b15-c941-3d1a-8b65-3fef291ca4ad/the-stock-market-may-be.html - -### BizTips: Boost your business by using the marketing funnel (source: Cape Cod Times) -Link: https://finance.yahoo.com/m/22db7f27-19ca-372f-9eb3-d6bfe6531a87/biztips%3A-boost-your-business.html - -### Sandisk (SNDK) Rockets 25.5%, Investors Makes Use of Market Bloodbath for Gains (source: Insider Monkey) -Link: https://finance.yahoo.com/news/sandisk-sndk-rockets-25-5-094041196.html - -### Goldman: AI PCs to buck 10% market slump as ‘edge computing’ demand accelerates (source: Investing.com) -Link: https://finance.yahoo.com/news/goldman-ai-pcs-buck-10-003503068.html - -### Fed to weigh interest rates amid Iran war, potential price increases (source: USA TODAY) -Link: https://finance.yahoo.com/m/fd7d2f56-6374-324a-87a7-ead11eed5d62/fed-to-weigh-interest-rates.html - -### Is Mobileye (MBLY) Now Offering Value After A 49% One Year Share Price Decline (source: Simply Wall St.) -Link: https://finance.yahoo.com/news/mobileye-mbly-now-offering-value-080612183.html - -### Will the Trump Bull Market Come to an Abrupt End Due to the Iran War? History Offers Its Objective and Potentially Uncomfortable Take. (source: Motley Fool) -Link: https://finance.yahoo.com/m/1b1369ec-855e-3380-9ed4-274a58d0c2be/will-the-trump-bull-market.html - -### 3 Magnificent High-Yield Dividend Stocks to Buy and Hold (source: Motley Fool) -Link: https://finance.yahoo.com/m/1f5939f6-20c4-3c84-ac7a-f41d6218897c/3-magnificent-high-yield.html - diff --git a/tests/test_scanner_complete_e2e.py b/tests/test_scanner_complete_e2e.py index f43ee718..2612065f 100644 --- a/tests/test_scanner_complete_e2e.py +++ b/tests/test_scanner_complete_e2e.py @@ -15,7 +15,7 @@ import pytest # Set up the Python path to include the project root import sys -sys.path.insert(0, '/Users/Ahmet/Repo/TradingAgents') +sys.path.insert(0, str(Path(__file__).parent.parent)) from tradingagents.agents.utils.scanner_tools import ( get_market_movers, diff --git a/tradingagents/dataflows/alpha_vantage_scanner.py b/tradingagents/dataflows/alpha_vantage_scanner.py index b6954824..06b49707 100644 --- a/tradingagents/dataflows/alpha_vantage_scanner.py +++ b/tradingagents/dataflows/alpha_vantage_scanner.py @@ -25,7 +25,7 @@ def get_market_movers_alpha_vantage( return f"Invalid category '{category}'. Must be one of: day_gainers, day_losers, most_actives" if category == 'most_actives': - return "Alpha Vantage does not support 'most_actives' category. Please use yfinance instead." + return "Error: Alpha Vantage does not support 'most_actives'. Use yfinance (default vendor) for this category." # Make API request for TOP_GAINERS_LOSERS endpoint response = _make_api_request("TOP_GAINERS_LOSERS", {}) @@ -38,7 +38,7 @@ def get_market_movers_alpha_vantage( return f"Error from Alpha Vantage: {data['Error Message']}" if "Note" in data: - return f"Alpha Vantage API limit reached: {data['Note']}" + return f"Error: Alpha Vantage API limit reached: {data['Note']}" # Map category to Alpha Vantage response key if category == 'day_gainers': @@ -46,7 +46,7 @@ def get_market_movers_alpha_vantage( elif category == 'day_losers': key = 'top_losers' else: - return f"Unsupported category: {category}" + return f"Error: unsupported category '{category}'" if key not in data: return f"No data found for {category}" @@ -74,7 +74,7 @@ def get_market_movers_alpha_vantage( if isinstance(price, str): try: price = f"${float(price):.2f}" - except: + except (ValueError, TypeError): pass if isinstance(change_pct, str): change_pct = change_pct.rstrip('%') # Remove % if present @@ -83,7 +83,7 @@ def get_market_movers_alpha_vantage( if isinstance(volume, (int, str)): try: volume = f"{int(volume):,}" - except: + except (ValueError, TypeError): pass result_str += f"| {symbol} | {price} | {change_pct} | {volume} |\n" diff --git a/tradingagents/dataflows/interface.py b/tradingagents/dataflows/interface.py index b4bdb71a..908e99db 100644 --- a/tradingagents/dataflows/interface.py +++ b/tradingagents/dataflows/interface.py @@ -191,8 +191,7 @@ def route_to_vendor(method: str, *args, **kwargs): try: return impl_func(*args, **kwargs) - except Exception: - # Continue to next vendor on any exception - continue + except AlphaVantageRateLimitError: + continue # Only rate limits trigger fallback raise RuntimeError(f"No available vendor for '{method}'") \ No newline at end of file diff --git a/tradingagents/dataflows/yfinance_scanner.py b/tradingagents/dataflows/yfinance_scanner.py index 205b727b..dd127918 100644 --- a/tradingagents/dataflows/yfinance_scanner.py +++ b/tradingagents/dataflows/yfinance_scanner.py @@ -31,7 +31,7 @@ def get_market_movers_yfinance( # Use yfinance screener module's screen function data = yf.screener.screen(screener_keys[category], count=25) - if not data or 'quotes' not in data: + if not data or not isinstance(data, dict) or 'quotes' not in data: return f"No data found for {category}" quotes = data['quotes'] @@ -97,29 +97,46 @@ def get_market_indices_yfinance() -> str: result_str += "| Index | Current Price | Change | Change % | 52W High | 52W Low |\n" result_str += "|-------|---------------|--------|----------|----------|----------|\n" + # Batch-download 1-day history for all symbols in a single request + symbols = list(indices.keys()) + indices_history = yf.download(symbols, period="2d", auto_adjust=True, progress=False, threads=True) + for symbol, name in indices.items(): try: ticker = yf.Ticker(symbol) - info = ticker.info - hist = ticker.history(period="1d") - - if hist.empty: + # fast_info is a lightweight cached property (no extra HTTP call) + fast = ticker.fast_info + + # Extract history for this symbol from the batch download + try: + if len(symbols) > 1: + closes = indices_history["Close"][symbol].dropna() + else: + closes = indices_history["Close"].dropna() + except KeyError: + closes = None + + if closes is None or len(closes) == 0: + result_str += f"| {name} | N/A | - | - | - | - |\n" continue - - current_price = hist['Close'].iloc[-1] - prev_close = info.get('previousClose', current_price) + + current_price = closes.iloc[-1] + prev_close = closes.iloc[-2] if len(closes) >= 2 else fast.previous_close + if prev_close is None or prev_close == 0: + prev_close = current_price + change = current_price - prev_close change_pct = (change / prev_close * 100) if prev_close else 0 - - high_52w = info.get('fiftyTwoWeekHigh', 'N/A') - low_52w = info.get('fiftyTwoWeekLow', 'N/A') - + + high_52w = fast.year_high + low_52w = fast.year_low + # Format numbers current_str = f"{current_price:.2f}" change_str = f"{change:+.2f}" change_pct_str = f"{change_pct:+.2f}%" - high_str = f"{high_52w:.2f}" if isinstance(high_52w, (int, float)) else high_52w - low_str = f"{low_52w:.2f}" if isinstance(low_52w, (int, float)) else low_52w + high_str = f"{high_52w:.2f}" if isinstance(high_52w, (int, float)) else str(high_52w) + low_str = f"{low_52w:.2f}" if isinstance(low_52w, (int, float)) else str(low_52w) result_str += f"| {name} | {current_str} | {change_str} | {change_pct_str} | {high_str} | {low_str} |\n" @@ -140,7 +157,9 @@ def get_sector_performance_yfinance() -> str: Formatted string containing sector performance data """ try: - # Get all GICS sectors + # All 11 standard GICS (Global Industry Classification Standard) sectors. + # These keys are fixed by yfinance's Sector API and cannot be fetched + # dynamically; the GICS taxonomy is maintained by MSCI/S&P and is stable. sector_keys = [ "communication-services", "consumer-cyclical", diff --git a/tradingagents/graph/scanner_conditional_logic.py b/tradingagents/graph/scanner_conditional_logic.py index 1c6f24a4..6ba4485c 100644 --- a/tradingagents/graph/scanner_conditional_logic.py +++ b/tradingagents/graph/scanner_conditional_logic.py @@ -3,57 +3,47 @@ from typing import Any from tradingagents.agents.utils.scanner_states import ScannerState +_ERROR_PREFIXES = ("Error", "No data", "No quotes", "No movers", "No news", "No industry", "Invalid") + + +def _report_is_valid(report: str) -> bool: + """Return True when *report* contains usable data (non-empty, non-error).""" + if not report or not report.strip(): + return False + return not any(report.startswith(prefix) for prefix in _ERROR_PREFIXES) + + class ScannerConditionalLogic: """Conditional logic for scanner graph flow control.""" def should_continue_geopolitical(self, state: ScannerState) -> bool: """ Determine if geopolitical scanning should continue. - - Args: - state: Current scanner state - - Returns: - bool: Whether to continue geopolitical scanning + + Returns True only when the geopolitical report contains usable data. """ - # Always continue for initial scan - no filtering logic implemented - return True + return _report_is_valid(state.get("geopolitical_report", "")) def should_continue_movers(self, state: ScannerState) -> bool: """ Determine if market movers scanning should continue. - - Args: - state: Current scanner state - - Returns: - bool: Whether to continue market movers scanning + + Returns True only when the market movers report contains usable data. """ - # Always continue for initial scan - no filtering logic implemented - return True + return _report_is_valid(state.get("market_movers_report", "")) def should_continue_sector(self, state: ScannerState) -> bool: """ Determine if sector scanning should continue. - - Args: - state: Current scanner state - - Returns: - bool: Whether to continue sector scanning + + Returns True only when the sector performance report contains usable data. """ - # Always continue for initial scan - no filtering logic implemented - return True + return _report_is_valid(state.get("sector_performance_report", "")) def should_continue_industry(self, state: ScannerState) -> bool: """ Determine if industry deep dive should continue. - - Args: - state: Current scanner state - - Returns: - bool: Whether to continue industry deep dive + + Returns True only when the industry deep dive report contains usable data. """ - # Always continue for initial scan - no filtering logic implemented - return True \ No newline at end of file + return _report_is_valid(state.get("industry_deep_dive_report", ""))