1713 lines
65 KiB
Python
1713 lines
65 KiB
Python
import datetime
|
|
from functools import wraps
|
|
from pathlib import Path
|
|
|
|
import typer
|
|
from dotenv import load_dotenv
|
|
from rich.console import Console, Group
|
|
|
|
# Load environment variables from .env file
|
|
load_dotenv()
|
|
from collections import deque
|
|
|
|
from rich import box
|
|
from rich.align import Align
|
|
from rich.columns import Columns
|
|
from rich.layout import Layout
|
|
from rich.live import Live
|
|
from rich.markdown import Markdown
|
|
from rich.panel import Panel
|
|
from rich.spinner import Spinner
|
|
from rich.table import Table
|
|
from rich.text import Text
|
|
|
|
from cli.models import AnalystType
|
|
from cli.utils import *
|
|
from tradingagents.default_config import DEFAULT_CONFIG
|
|
from tradingagents.graph.discovery_graph import DiscoveryGraph
|
|
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
|
|
|
console = Console()
|
|
|
|
app = typer.Typer(
|
|
name="TradingAgents",
|
|
help="TradingAgents CLI: Multi-Agents LLM Financial Trading Framework",
|
|
add_completion=True, # Enable shell completion
|
|
)
|
|
|
|
|
|
def extract_text_from_content(content):
|
|
"""
|
|
Extract plain text from LangChain content blocks.
|
|
|
|
Args:
|
|
content: Either a string or a list of content blocks from LangChain
|
|
|
|
Returns:
|
|
str: Extracted text
|
|
"""
|
|
if isinstance(content, str):
|
|
return content
|
|
elif isinstance(content, list):
|
|
text_parts = []
|
|
for block in content:
|
|
if isinstance(block, dict) and "text" in block:
|
|
text_parts.append(block["text"])
|
|
elif isinstance(block, str):
|
|
text_parts.append(block)
|
|
return "\n".join(text_parts)
|
|
else:
|
|
return str(content)
|
|
|
|
|
|
# Create a deque to store recent messages with a maximum length
|
|
class MessageBuffer:
|
|
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 = {
|
|
# Analyst Team
|
|
"Market Analyst": "pending",
|
|
"Social Analyst": "pending",
|
|
"News Analyst": "pending",
|
|
"Fundamentals Analyst": "pending",
|
|
# Research Team
|
|
"Bull Researcher": "pending",
|
|
"Bear Researcher": "pending",
|
|
"Research Manager": "pending",
|
|
# Trading Team
|
|
"Trader": "pending",
|
|
# Risk Management Team
|
|
"Risky Analyst": "pending",
|
|
"Neutral Analyst": "pending",
|
|
"Safe Analyst": "pending",
|
|
# Final Decision
|
|
"Portfolio Manager": "pending",
|
|
}
|
|
self.current_agent = None
|
|
self.report_sections = {
|
|
"market_report": None,
|
|
"sentiment_report": None,
|
|
"news_report": None,
|
|
"fundamentals_report": None,
|
|
"investment_plan": None,
|
|
"trader_investment_plan": None,
|
|
"final_trade_decision": None,
|
|
}
|
|
|
|
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": "Final Trade 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
|
|
if any(
|
|
self.report_sections[section]
|
|
for section in [
|
|
"market_report",
|
|
"sentiment_report",
|
|
"news_report",
|
|
"fundamentals_report",
|
|
]
|
|
):
|
|
report_parts.append("## Analyst Team Reports")
|
|
if self.report_sections["market_report"]:
|
|
report_parts.append(f"### Market Analysis\n{self.report_sections['market_report']}")
|
|
if self.report_sections["sentiment_report"]:
|
|
report_parts.append(
|
|
f"### Social Sentiment\n{self.report_sections['sentiment_report']}"
|
|
)
|
|
if self.report_sections["news_report"]:
|
|
report_parts.append(f"### News Analysis\n{self.report_sections['news_report']}")
|
|
if self.report_sections["fundamentals_report"]:
|
|
report_parts.append(
|
|
f"### Fundamentals Analysis\n{self.report_sections['fundamentals_report']}"
|
|
)
|
|
|
|
# Research Team Reports
|
|
if self.report_sections["investment_plan"]:
|
|
report_parts.append("## Research Team Decision")
|
|
report_parts.append(f"{self.report_sections['investment_plan']}")
|
|
|
|
# Trading Team Reports
|
|
if self.report_sections["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["final_trade_decision"]:
|
|
report_parts.append("## Final Trade 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 update_display(layout, spinner_text=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
|
|
teams = {
|
|
"Analyst Team": [
|
|
"Market Analyst",
|
|
"Social Analyst",
|
|
"News Analyst",
|
|
"Fundamentals Analyst",
|
|
],
|
|
"Research Team": ["Bull Researcher", "Bear Researcher", "Research Manager"],
|
|
"Trading Team": ["Trader"],
|
|
"Risk Management": ["Risky Analyst", "Neutral Analyst", "Safe Analyst"],
|
|
"Final Decision": ["Portfolio Manager"],
|
|
}
|
|
|
|
for team, agents in teams.items():
|
|
# Add first agent with team name
|
|
first_agent = agents[0]
|
|
status = message_buffer.agent_status[first_agent]
|
|
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[agent]
|
|
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:
|
|
# Truncate tool call args if too long
|
|
if isinstance(args, str) and len(args) > 100:
|
|
args = args[:97] + "..."
|
|
all_messages.append((timestamp, "Tool", f"{tool_name}: {args}"))
|
|
|
|
# Add regular messages
|
|
for timestamp, msg_type, content in message_buffer.messages:
|
|
# Convert content to string if it's not already
|
|
content_str = content
|
|
if isinstance(content, list):
|
|
# Handle list of content blocks (Anthropic format)
|
|
text_parts = []
|
|
for item in content:
|
|
if isinstance(item, dict):
|
|
if item.get("type") == "text":
|
|
text_parts.append(item.get("text", ""))
|
|
elif item.get("type") == "tool_use":
|
|
text_parts.append(f"[Tool: {item.get('name', 'unknown')}]")
|
|
else:
|
|
text_parts.append(str(item))
|
|
content_str = " ".join(text_parts)
|
|
elif not isinstance(content_str, str):
|
|
content_str = str(content)
|
|
|
|
# Truncate message content if too long
|
|
if len(content_str) > 200:
|
|
content_str = content_str[:197] + "..."
|
|
all_messages.append((timestamp, msg_type, content_str))
|
|
|
|
# Sort by timestamp
|
|
all_messages.sort(key=lambda x: x[0])
|
|
|
|
# Calculate how many messages we can show based on available space
|
|
# Start with a reasonable number and adjust based on content length
|
|
max_messages = 12 # Increased from 8 to better fill the space
|
|
|
|
# Get the last N messages that will fit in the panel
|
|
recent_messages = all_messages[-max_messages:]
|
|
|
|
# Add messages to table
|
|
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)
|
|
|
|
if spinner_text:
|
|
messages_table.add_row("", "Spinner", spinner_text)
|
|
|
|
# Add a footer to indicate if messages were truncated
|
|
if len(all_messages) > max_messages:
|
|
messages_table.footer = (
|
|
f"[dim]Showing last {max_messages} of {len(all_messages)} messages[/dim]"
|
|
)
|
|
|
|
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
|
|
tool_calls_count = len(message_buffer.tool_calls)
|
|
llm_calls_count = sum(
|
|
1 for _, msg_type, _ in message_buffer.messages if msg_type == "Reasoning"
|
|
)
|
|
reports_count = sum(
|
|
1 for content in message_buffer.report_sections.values() if content is not None
|
|
)
|
|
|
|
stats_table = Table(show_header=False, box=None, padding=(0, 2), expand=True)
|
|
stats_table.add_column("Stats", justify="center")
|
|
stats_table.add_row(
|
|
f"Tool Calls: {tool_calls_count} | LLM Calls: {llm_calls_count} | Generated Reports: {reports_count}"
|
|
)
|
|
|
|
layout["footer"].update(Panel(stats_table, border_style="grey50"))
|
|
|
|
|
|
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") 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. Final Decision\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() # Add a blank line after the welcome box
|
|
|
|
# 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: Select mode (Discovery or Trading)
|
|
console.print(create_question_box("Step 1: Mode Selection", "Select which agent to run"))
|
|
mode = select_mode()
|
|
|
|
# Step 2: Ticker symbol (only for Trading mode)
|
|
selected_ticker = None
|
|
if mode == "trading":
|
|
console.print(
|
|
create_question_box(
|
|
"Step 2: Ticker Symbol", "Enter the ticker symbol to analyze", "SPY"
|
|
)
|
|
)
|
|
selected_ticker = get_ticker()
|
|
|
|
# Step 3: Analysis date
|
|
default_date = datetime.datetime.now().strftime("%Y-%m-%d")
|
|
step_number = 2 if mode == "discovery" else 3
|
|
console.print(
|
|
create_question_box(
|
|
f"Step {step_number}: Analysis Date",
|
|
"Enter the analysis date (YYYY-MM-DD)",
|
|
default_date,
|
|
)
|
|
)
|
|
analysis_date = get_analysis_date()
|
|
|
|
# For trading mode, continue with analyst selection
|
|
selected_analysts = None
|
|
selected_research_depth = None
|
|
if mode == "trading":
|
|
# Step 4: Select analysts
|
|
console.print(
|
|
create_question_box(
|
|
"Step 4: 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 5: Research depth
|
|
console.print(
|
|
create_question_box("Step 5: Research Depth", "Select your research depth level")
|
|
)
|
|
selected_research_depth = select_research_depth()
|
|
step_offset = 5
|
|
else:
|
|
step_offset = 2
|
|
|
|
# OpenAI backend
|
|
console.print(
|
|
create_question_box(
|
|
f"Step {step_offset + 1}: OpenAI backend", "Select which service to talk to"
|
|
)
|
|
)
|
|
selected_llm_provider, backend_url = select_llm_provider()
|
|
|
|
# Thinking agents
|
|
console.print(
|
|
create_question_box(
|
|
f"Step {step_offset + 2}: Thinking Agents", "Select your thinking agents for analysis"
|
|
)
|
|
)
|
|
selected_shallow_thinker = select_shallow_thinking_agent(selected_llm_provider)
|
|
selected_deep_thinker = select_deep_thinking_agent(selected_llm_provider)
|
|
|
|
return {
|
|
"mode": mode,
|
|
"ticker": selected_ticker,
|
|
"analysis_date": analysis_date,
|
|
"analysts": selected_analysts,
|
|
"research_depth": selected_research_depth,
|
|
"llm_provider": selected_llm_provider.lower(),
|
|
"backend_url": backend_url,
|
|
"shallow_thinker": selected_shallow_thinker,
|
|
"deep_thinker": selected_deep_thinker,
|
|
}
|
|
|
|
|
|
def get_ticker():
|
|
"""Get ticker symbol from user input."""
|
|
return typer.prompt("", default="SPY")
|
|
|
|
|
|
def get_analysis_date():
|
|
"""Get the analysis date from user input."""
|
|
while True:
|
|
date_str = typer.prompt("", default=datetime.datetime.now().strftime("%Y-%m-%d"))
|
|
try:
|
|
# Validate date format and ensure it's not in the future
|
|
analysis_date = datetime.datetime.strptime(date_str, "%Y-%m-%d")
|
|
if analysis_date.date() > datetime.datetime.now().date():
|
|
console.print("[red]Error: Analysis date cannot be in the future[/red]")
|
|
continue
|
|
return date_str
|
|
except ValueError:
|
|
console.print("[red]Error: Invalid date format. Please use YYYY-MM-DD[/red]")
|
|
|
|
|
|
def select_mode():
|
|
"""Select between Discovery and Trading mode."""
|
|
console.print("[1] Discovery - Find investment opportunities")
|
|
console.print("[2] Trading - Analyze a specific ticker")
|
|
|
|
while True:
|
|
choice = typer.prompt("Select mode", default="2")
|
|
if choice in ["1", "2"]:
|
|
return "discovery" if choice == "1" else "trading"
|
|
console.print("[red]Invalid choice. Please enter 1 or 2[/red]")
|
|
|
|
|
|
def display_complete_report(final_state):
|
|
"""Display the complete analysis report with team-based panels."""
|
|
console.print("\n[bold green]Complete Analysis Report[/bold green]\n")
|
|
|
|
# I. Analyst Team Reports
|
|
analyst_reports = []
|
|
|
|
# Market Analyst Report
|
|
if final_state.get("market_report"):
|
|
analyst_reports.append(
|
|
Panel(
|
|
Markdown(extract_text_from_content(final_state["market_report"])),
|
|
title="Market Analyst",
|
|
border_style="blue",
|
|
padding=(1, 2),
|
|
)
|
|
)
|
|
|
|
# Social Analyst Report
|
|
if final_state.get("sentiment_report"):
|
|
analyst_reports.append(
|
|
Panel(
|
|
Markdown(extract_text_from_content(final_state["sentiment_report"])),
|
|
title="Social Analyst",
|
|
border_style="blue",
|
|
padding=(1, 2),
|
|
)
|
|
)
|
|
|
|
# News Analyst Report
|
|
if final_state.get("news_report"):
|
|
analyst_reports.append(
|
|
Panel(
|
|
Markdown(extract_text_from_content(final_state["news_report"])),
|
|
title="News Analyst",
|
|
border_style="blue",
|
|
padding=(1, 2),
|
|
)
|
|
)
|
|
|
|
# Fundamentals Analyst Report
|
|
if final_state.get("fundamentals_report"):
|
|
analyst_reports.append(
|
|
Panel(
|
|
Markdown(extract_text_from_content(final_state["fundamentals_report"])),
|
|
title="Fundamentals Analyst",
|
|
border_style="blue",
|
|
padding=(1, 2),
|
|
)
|
|
)
|
|
|
|
if analyst_reports:
|
|
console.print(
|
|
Panel(
|
|
Columns(analyst_reports, equal=True, expand=True),
|
|
title="I. Analyst Team Reports",
|
|
border_style="cyan",
|
|
padding=(1, 2),
|
|
)
|
|
)
|
|
|
|
# II. Research Team Reports
|
|
if final_state.get("investment_debate_state"):
|
|
research_reports = []
|
|
debate_state = final_state["investment_debate_state"]
|
|
|
|
# Bull Researcher Analysis
|
|
if debate_state.get("bull_history"):
|
|
research_reports.append(
|
|
Panel(
|
|
Markdown(debate_state["bull_history"]),
|
|
title="Bull Researcher",
|
|
border_style="blue",
|
|
padding=(1, 2),
|
|
)
|
|
)
|
|
|
|
# Bear Researcher Analysis
|
|
if debate_state.get("bear_history"):
|
|
research_reports.append(
|
|
Panel(
|
|
Markdown(debate_state["bear_history"]),
|
|
title="Bear Researcher",
|
|
border_style="blue",
|
|
padding=(1, 2),
|
|
)
|
|
)
|
|
|
|
# Research Manager Decision
|
|
if debate_state.get("judge_decision"):
|
|
research_reports.append(
|
|
Panel(
|
|
Markdown(extract_text_from_content(debate_state["judge_decision"])),
|
|
title="Research Manager",
|
|
border_style="blue",
|
|
padding=(1, 2),
|
|
)
|
|
)
|
|
|
|
if research_reports:
|
|
console.print(
|
|
Panel(
|
|
Columns(research_reports, equal=True, expand=True),
|
|
title="II. Research Team Decision",
|
|
border_style="magenta",
|
|
padding=(1, 2),
|
|
)
|
|
)
|
|
|
|
# III. Trading Team Reports
|
|
if final_state.get("trader_investment_plan"):
|
|
console.print(
|
|
Panel(
|
|
Panel(
|
|
Markdown(extract_text_from_content(final_state["trader_investment_plan"])),
|
|
title="Trader",
|
|
border_style="blue",
|
|
padding=(1, 2),
|
|
),
|
|
title="III. Trading Team Plan",
|
|
border_style="yellow",
|
|
padding=(1, 2),
|
|
)
|
|
)
|
|
|
|
# IV. Risk Management Team Reports
|
|
if final_state.get("risk_debate_state"):
|
|
risk_reports = []
|
|
risk_state = final_state["risk_debate_state"]
|
|
|
|
# Aggressive (Risky) Analyst Analysis
|
|
if risk_state.get("risky_history"):
|
|
risk_reports.append(
|
|
Panel(
|
|
Markdown(risk_state["risky_history"]),
|
|
title="Aggressive Analyst",
|
|
border_style="blue",
|
|
padding=(1, 2),
|
|
)
|
|
)
|
|
|
|
# Risk Audit (Safe) Analyst Analysis
|
|
if risk_state.get("safe_history"):
|
|
risk_reports.append(
|
|
Panel(
|
|
Markdown(risk_state["safe_history"]),
|
|
title="Risk Audit Analyst",
|
|
border_style="blue",
|
|
padding=(1, 2),
|
|
)
|
|
)
|
|
|
|
# Neutral Analyst Analysis
|
|
if risk_state.get("neutral_history"):
|
|
risk_reports.append(
|
|
Panel(
|
|
Markdown(risk_state["neutral_history"]),
|
|
title="Neutral Analyst",
|
|
border_style="blue",
|
|
padding=(1, 2),
|
|
)
|
|
)
|
|
|
|
if risk_reports:
|
|
console.print(
|
|
Panel(
|
|
Columns(risk_reports, equal=True, expand=True),
|
|
title="IV. Risk Management Team Decision",
|
|
border_style="red",
|
|
padding=(1, 2),
|
|
)
|
|
)
|
|
|
|
# V. Final Trade Decision
|
|
if risk_state.get("judge_decision"):
|
|
console.print(
|
|
Panel(
|
|
Panel(
|
|
Markdown(extract_text_from_content(risk_state["judge_decision"])),
|
|
title="Final Decider",
|
|
border_style="blue",
|
|
padding=(1, 2),
|
|
),
|
|
title="V. Final Trade Decision",
|
|
border_style="green",
|
|
padding=(1, 2),
|
|
)
|
|
)
|
|
|
|
|
|
def update_research_team_status(status):
|
|
"""Update status for all research team members and trader."""
|
|
research_team = ["Bull Researcher", "Bear Researcher", "Research Manager", "Trader"]
|
|
for agent in research_team:
|
|
message_buffer.update_agent_status(agent, status)
|
|
|
|
|
|
def extract_text_from_content(content):
|
|
"""Extract text string from content that may be a string or list of dicts.
|
|
|
|
Handles both:
|
|
- Plain strings
|
|
- Lists of dicts with 'type': 'text' and 'text': '...'
|
|
"""
|
|
if isinstance(content, str):
|
|
return content
|
|
elif isinstance(content, list):
|
|
text_parts = []
|
|
for item in content:
|
|
if isinstance(item, dict) and item.get("type") == "text":
|
|
text_parts.append(item.get("text", ""))
|
|
return "\n".join(text_parts) if text_parts else str(content)
|
|
else:
|
|
return str(content)
|
|
|
|
|
|
def extract_content_string(content):
|
|
"""Extract string content from various message formats."""
|
|
if isinstance(content, str):
|
|
return content
|
|
elif isinstance(content, list):
|
|
# Handle Anthropic's list format
|
|
text_parts = []
|
|
for item in content:
|
|
if isinstance(item, dict):
|
|
if item.get("type") == "text":
|
|
text_parts.append(item.get("text", ""))
|
|
elif item.get("type") == "tool_use":
|
|
text_parts.append(f"[Tool: {item.get('name', 'unknown')}]")
|
|
else:
|
|
text_parts.append(str(item))
|
|
return " ".join(text_parts)
|
|
else:
|
|
return str(content)
|
|
|
|
|
|
def format_movement_stats(movement: dict) -> str:
|
|
"""Format movement stats for display in discovery ranking panels."""
|
|
if not movement:
|
|
return ""
|
|
|
|
def fmt(value):
|
|
if value is None:
|
|
return "N/A"
|
|
return f"{value:+.2f}%"
|
|
|
|
return (
|
|
"**Movement:** "
|
|
f"1D {fmt(movement.get('1d'))} | "
|
|
f"7D {fmt(movement.get('7d'))} | "
|
|
f"1M {fmt(movement.get('1m'))} | "
|
|
f"6M {fmt(movement.get('6m'))} | "
|
|
f"1Y {fmt(movement.get('1y'))}"
|
|
)
|
|
|
|
|
|
def run_analysis():
|
|
# First get all user selections
|
|
selections = get_user_selections()
|
|
|
|
# Branch based on mode
|
|
if selections["mode"] == "discovery":
|
|
run_discovery_analysis(selections)
|
|
else:
|
|
run_trading_analysis(selections)
|
|
|
|
|
|
def run_discovery_analysis(selections):
|
|
"""Run discovery mode to find investment opportunities."""
|
|
import json
|
|
|
|
from tradingagents.dataflows.config import set_config
|
|
|
|
# Create config
|
|
config = DEFAULT_CONFIG.copy()
|
|
config["quick_think_llm"] = selections["shallow_thinker"]
|
|
config["deep_think_llm"] = selections["deep_thinker"]
|
|
config["backend_url"] = selections["backend_url"]
|
|
config["llm_provider"] = selections["llm_provider"].lower()
|
|
|
|
# Set config globally for route_to_vendor
|
|
set_config(config)
|
|
|
|
# Generate run timestamp
|
|
import datetime
|
|
|
|
run_timestamp = datetime.datetime.now().strftime("%H_%M_%S")
|
|
|
|
# Create results directory with run timestamp
|
|
results_dir = (
|
|
Path(config["results_dir"])
|
|
/ "discovery"
|
|
/ selections["analysis_date"]
|
|
/ f"run_{run_timestamp}"
|
|
)
|
|
results_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Add results dir to config so graph can use it for logging
|
|
config["discovery_run_dir"] = str(results_dir)
|
|
|
|
console.print(
|
|
f"[dim]Using {config['llm_provider'].upper()} - Shallow: {config['quick_think_llm']}, Deep: {config['deep_think_llm']}[/dim]"
|
|
)
|
|
|
|
# Initialize Discovery Graph (LLMs initialized internally like TradingAgentsGraph)
|
|
discovery_graph = DiscoveryGraph(config=config)
|
|
|
|
console.print(
|
|
f"\n[bold green]Running Discovery Analysis for {selections['analysis_date']}[/bold green]\n"
|
|
)
|
|
|
|
# Run discovery (uses run() method which saves results)
|
|
result = discovery_graph.run(trade_date=selections["analysis_date"])
|
|
|
|
# Get final ranking for display (results saved by discovery_graph.run())
|
|
final_ranking = result.get("final_ranking", "No ranking available")
|
|
|
|
rankings_list = []
|
|
# Format rankings for console display
|
|
try:
|
|
if isinstance(final_ranking, str):
|
|
rankings = json.loads(final_ranking)
|
|
else:
|
|
rankings = final_ranking
|
|
|
|
# Handle dict with 'rankings' key
|
|
if isinstance(rankings, dict):
|
|
rankings = rankings.get("rankings", [])
|
|
rankings_list = rankings
|
|
|
|
# Build nicely formatted markdown
|
|
formatted_output = []
|
|
for rank in rankings:
|
|
ticker = rank.get("ticker", "UNKNOWN")
|
|
company_name = rank.get("company_name", ticker)
|
|
current_price = rank.get("current_price")
|
|
description = rank.get("description", "")
|
|
strategy = rank.get("strategy_match", "N/A")
|
|
final_score = rank.get("final_score", 0)
|
|
confidence = rank.get("confidence", 0)
|
|
reason = rank.get("reason", "")
|
|
rank_num = rank.get("rank", "?")
|
|
|
|
price_str = f"${current_price:.2f}" if current_price else "N/A"
|
|
|
|
formatted_output.append(f"### #{rank_num}: {ticker} - {company_name}")
|
|
formatted_output.append("")
|
|
formatted_output.append(
|
|
f"**Price:** {price_str} | **Strategy:** {strategy} | **Score:** {final_score} | **Confidence:** {confidence}/10"
|
|
)
|
|
formatted_output.append("")
|
|
if description:
|
|
formatted_output.append(f"*{description}*")
|
|
formatted_output.append("")
|
|
formatted_output.append("**Investment Thesis:**")
|
|
formatted_output.append(f"{reason}")
|
|
formatted_output.append("")
|
|
formatted_output.append("---")
|
|
formatted_output.append("")
|
|
|
|
final_ranking_text = "\n".join(formatted_output)
|
|
except Exception:
|
|
# Fallback to raw text
|
|
final_ranking_text = extract_text_from_content(final_ranking)
|
|
|
|
console.print(f"\n[dim]Results saved to: {results_dir}[/dim]\n")
|
|
|
|
# Display results
|
|
if getattr(discovery_graph, "console_price_charts", False) and rankings_list:
|
|
window_order = [
|
|
str(window).strip().lower()
|
|
for window in getattr(discovery_graph, "price_chart_windows", ["1m"])
|
|
]
|
|
original_chart_width = getattr(discovery_graph, "price_chart_width", 60)
|
|
try:
|
|
# Fit multiple window charts side-by-side when possible.
|
|
if window_order:
|
|
target_width = max(24, (console.size.width - 12) // max(1, len(window_order)))
|
|
discovery_graph.price_chart_width = min(original_chart_width, target_width)
|
|
bundle_map = discovery_graph.build_price_chart_bundle(rankings_list)
|
|
finally:
|
|
discovery_graph.price_chart_width = original_chart_width
|
|
for rank in rankings_list:
|
|
ticker = (rank.get("ticker") or "UNKNOWN").upper()
|
|
company_name = rank.get("company_name", ticker)
|
|
current_price = rank.get("current_price")
|
|
description = rank.get("description", "")
|
|
strategy = rank.get("strategy_match", "N/A")
|
|
final_score = rank.get("final_score", 0)
|
|
confidence = rank.get("confidence", 0)
|
|
reason = rank.get("reason", "")
|
|
rank_num = rank.get("rank", "?")
|
|
|
|
price_str = f"${current_price:.2f}" if current_price else "N/A"
|
|
ticker_bundle = bundle_map.get(ticker, {})
|
|
movement = ticker_bundle.get("movement", {})
|
|
movement_line = (
|
|
format_movement_stats(movement)
|
|
if getattr(discovery_graph, "price_chart_show_movement_stats", True)
|
|
else ""
|
|
)
|
|
|
|
lines = [
|
|
f"**Price:** {price_str} | **Strategy:** {strategy} | **Score:** {final_score} | **Confidence:** {confidence}/10",
|
|
]
|
|
if movement_line:
|
|
lines.append(movement_line)
|
|
if description:
|
|
lines.append(f"*{description}*")
|
|
lines.append("**Investment Thesis:**")
|
|
lines.append(reason)
|
|
per_rank_md = "\n\n".join(lines)
|
|
|
|
renderables = [Markdown(per_rank_md)]
|
|
charts = ticker_bundle.get("charts", {})
|
|
if charts:
|
|
chart_columns = []
|
|
for key in window_order:
|
|
chart = charts.get(key)
|
|
if chart:
|
|
chart_columns.append(Text.from_ansi(chart))
|
|
if chart_columns:
|
|
renderables.append(Columns(chart_columns, equal=True, expand=True))
|
|
else:
|
|
chart = ticker_bundle.get("chart")
|
|
if chart:
|
|
renderables.append(Text.from_ansi(chart))
|
|
|
|
console.print(
|
|
Panel(
|
|
Group(*renderables),
|
|
title=f"#{rank_num}: {ticker} - {company_name}",
|
|
border_style="green",
|
|
)
|
|
)
|
|
else:
|
|
console.print(
|
|
Panel(
|
|
(
|
|
Markdown(final_ranking_text)
|
|
if final_ranking_text
|
|
else "[yellow]No recommendations generated[/yellow]"
|
|
),
|
|
title="Top Investment Opportunities",
|
|
border_style="green",
|
|
)
|
|
)
|
|
|
|
# Extract tickers from the ranking using the discovery graph's LLM
|
|
discovered_tickers = extract_tickers_from_ranking(
|
|
final_ranking_text, discovery_graph.quick_thinking_llm
|
|
)
|
|
|
|
# Loop: Ask if they want to analyze any of the discovered tickers
|
|
while True:
|
|
if not discovered_tickers:
|
|
console.print("\n[yellow]No tickers found in discovery results[/yellow]")
|
|
break
|
|
|
|
console.print(f"\n[bold]Discovered tickers:[/bold] {', '.join(discovered_tickers)}")
|
|
|
|
run_trading = typer.confirm(
|
|
"\nWould you like to run trading analysis on one of these tickers?", default=False
|
|
)
|
|
|
|
if not run_trading:
|
|
console.print("\n[green]Discovery complete! Exiting...[/green]")
|
|
break
|
|
|
|
# Let user select a ticker
|
|
console.print("\n[bold]Select a ticker to analyze:[/bold]")
|
|
for i, ticker in enumerate(discovered_tickers, 1):
|
|
console.print(f"[{i}] {ticker}")
|
|
|
|
while True:
|
|
choice = typer.prompt("Enter number", default="1")
|
|
try:
|
|
idx = int(choice) - 1
|
|
if 0 <= idx < len(discovered_tickers):
|
|
selected_ticker = discovered_tickers[idx]
|
|
break
|
|
console.print("[red]Invalid choice. Try again.[/red]")
|
|
except ValueError:
|
|
console.print("[red]Invalid number. Try again.[/red]")
|
|
|
|
console.print(f"\n[green]Selected: {selected_ticker}[/green]\n")
|
|
|
|
# Update selections with the selected ticker
|
|
trading_selections = selections.copy()
|
|
trading_selections["ticker"] = selected_ticker
|
|
trading_selections["mode"] = "trading"
|
|
|
|
# If analysts weren't selected (discovery mode), select default
|
|
if not trading_selections.get("analysts"):
|
|
trading_selections["analysts"] = [
|
|
AnalystType("market"),
|
|
AnalystType("social"),
|
|
AnalystType("news"),
|
|
AnalystType("fundamentals"),
|
|
]
|
|
|
|
# If research depth wasn't selected, use default
|
|
if not trading_selections.get("research_depth"):
|
|
trading_selections["research_depth"] = 1
|
|
|
|
# Run trading analysis
|
|
run_trading_analysis(trading_selections)
|
|
|
|
console.print("\n" + "=" * 70 + "\n")
|
|
|
|
|
|
def extract_tickers_from_ranking(ranking_text, llm=None):
|
|
"""Extract ticker symbols from discovery ranking results using LLM.
|
|
|
|
Args:
|
|
ranking_text: The text containing ticker information
|
|
llm: Optional LLM instance to use for extraction. If None, falls back to regex.
|
|
|
|
Returns:
|
|
List of ticker symbols (uppercase strings)
|
|
"""
|
|
import json
|
|
import re
|
|
|
|
from langchain_core.messages import HumanMessage
|
|
|
|
# Try to extract from JSON first (fast path)
|
|
try:
|
|
# Look for JSON array in the text
|
|
json_match = re.search(r"\[[\s\S]*\]", ranking_text)
|
|
if json_match:
|
|
data = json.loads(json_match.group())
|
|
if isinstance(data, list):
|
|
tickers = [item.get("ticker", "").upper() for item in data if item.get("ticker")]
|
|
if tickers:
|
|
return tickers
|
|
except Exception:
|
|
pass
|
|
|
|
# Use LLM to extract tickers if available
|
|
if llm is not None:
|
|
try:
|
|
# Create extraction prompt
|
|
prompt = f"""Extract all stock ticker symbols from the following ranking text.
|
|
Return ONLY a comma-separated list of valid ticker symbols (1-5 uppercase letters).
|
|
Do not include explanations, just the tickers.
|
|
|
|
Examples of valid tickers: AAPL, GOOGL, MSFT, TSLA, NVDA
|
|
Examples of invalid: RMB (currency), BTC (crypto - not a stock ticker unless it's an ETF)
|
|
|
|
Text:
|
|
{ranking_text}
|
|
|
|
Tickers:"""
|
|
|
|
response = llm.invoke([HumanMessage(content=prompt)])
|
|
|
|
# Extract text from response
|
|
response_text = extract_text_from_content(response.content)
|
|
|
|
# Parse the comma-separated list
|
|
tickers = [t.strip().upper() for t in response_text.split(",") if t.strip()]
|
|
|
|
# Basic validation: 1-5 uppercase letters
|
|
valid_tickers = [t for t in tickers if re.match(r"^[A-Z]{1,5}$", t)]
|
|
|
|
# Remove duplicates while preserving order
|
|
seen = set()
|
|
unique_tickers = []
|
|
for t in valid_tickers:
|
|
if t not in seen:
|
|
seen.add(t)
|
|
unique_tickers.append(t)
|
|
|
|
return unique_tickers[:10] # Limit to first 10
|
|
|
|
except Exception as e:
|
|
console.print(
|
|
f"[yellow]Warning: LLM ticker extraction failed ({e}), using regex fallback[/yellow]"
|
|
)
|
|
|
|
# Regex fallback (used when no LLM provided or LLM extraction fails)
|
|
tickers = re.findall(r"\b[A-Z]{1,5}\b", ranking_text)
|
|
exclude = {
|
|
"THE",
|
|
"AND",
|
|
"OR",
|
|
"FOR",
|
|
"NOT",
|
|
"BUT",
|
|
"TOP",
|
|
"USD",
|
|
"USA",
|
|
"AI",
|
|
"IT",
|
|
"IS",
|
|
"AS",
|
|
"AT",
|
|
"IN",
|
|
"ON",
|
|
"TO",
|
|
"BY",
|
|
"RMB",
|
|
"BTC",
|
|
}
|
|
tickers = [t for t in tickers if t not in exclude]
|
|
seen = set()
|
|
unique_tickers = []
|
|
for t in tickers:
|
|
if t not in seen:
|
|
seen.add(t)
|
|
unique_tickers.append(t)
|
|
return unique_tickers[:10]
|
|
|
|
|
|
def run_trading_analysis(selections):
|
|
"""Run trading mode for a specific ticker."""
|
|
# 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"]
|
|
config["quick_think_llm"] = selections["shallow_thinker"]
|
|
config["deep_think_llm"] = selections["deep_thinker"]
|
|
config["backend_url"] = selections["backend_url"]
|
|
config["llm_provider"] = selections["llm_provider"].lower()
|
|
|
|
# Initialize the graph
|
|
graph = TradingAgentsGraph(
|
|
[analyst.value for analyst in selections["analysts"]], config=config, debug=True
|
|
)
|
|
|
|
# Create result directory
|
|
results_dir = (
|
|
Path(config["results_dir"]) / "trading" / selections["analysis_date"] / selections["ticker"]
|
|
)
|
|
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)
|
|
|
|
# IMPORTANT: `message_buffer` is a global singleton used by the Rich UI.
|
|
# When running multiple tickers in the same CLI session (e.g., discovery → trading → trading),
|
|
# we must reset any previously wrapped methods; otherwise decorators stack and later runs
|
|
# write logs/reports into earlier tickers' folders.
|
|
message_buffer.add_message = MessageBuffer.add_message.__get__(message_buffer, MessageBuffer)
|
|
message_buffer.add_tool_call = MessageBuffer.add_tool_call.__get__(
|
|
message_buffer, MessageBuffer
|
|
)
|
|
message_buffer.update_report_section = MessageBuffer.update_report_section.__get__(
|
|
message_buffer, MessageBuffer
|
|
)
|
|
|
|
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") 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") 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") as f:
|
|
# Extract text from LangChain content blocks
|
|
content_text = extract_text_from_content(content)
|
|
f.write(content_text)
|
|
|
|
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"
|
|
)
|
|
|
|
# Reset UI buffers for a clean per-ticker run
|
|
message_buffer.messages.clear()
|
|
message_buffer.tool_calls.clear()
|
|
|
|
# Now start the display layout
|
|
layout = create_layout()
|
|
|
|
with Live(layout, refresh_per_second=4) as live:
|
|
# Initial display
|
|
update_display(layout)
|
|
|
|
# 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)
|
|
|
|
# Reset agent statuses
|
|
for agent in message_buffer.agent_status:
|
|
message_buffer.update_agent_status(agent, "pending")
|
|
|
|
# Reset report sections
|
|
for section in message_buffer.report_sections:
|
|
message_buffer.report_sections[section] = None
|
|
message_buffer.current_report = None
|
|
message_buffer.final_report = None
|
|
|
|
# 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)
|
|
|
|
# Create spinner text
|
|
spinner_text = f"Analyzing {selections['ticker']} on {selections['analysis_date']}..."
|
|
update_display(layout, spinner_text)
|
|
|
|
# Initialize state and get graph args
|
|
init_agent_state = graph.propagator.create_initial_state(
|
|
selections["ticker"], selections["analysis_date"]
|
|
)
|
|
args = graph.propagator.get_graph_args()
|
|
|
|
# Stream the analysis
|
|
trace = []
|
|
for chunk in graph.graph.stream(init_agent_state, **args):
|
|
if len(chunk["messages"]) > 0:
|
|
# Get the last message from the chunk
|
|
last_message = chunk["messages"][-1]
|
|
|
|
# Extract message content and type
|
|
if hasattr(last_message, "content"):
|
|
content = extract_content_string(
|
|
last_message.content
|
|
) # Use the helper function
|
|
msg_type = "Reasoning"
|
|
else:
|
|
content = str(last_message)
|
|
msg_type = "System"
|
|
|
|
# Add message to buffer
|
|
message_buffer.add_message(msg_type, content)
|
|
|
|
# If it's a tool call, add it to tool calls
|
|
if hasattr(last_message, "tool_calls"):
|
|
for tool_call in last_message.tool_calls:
|
|
# Handle both dictionary and object 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 reports and agent status based on chunk content
|
|
# Analyst Team Reports
|
|
if "market_report" in chunk and chunk["market_report"]:
|
|
message_buffer.update_report_section("market_report", chunk["market_report"])
|
|
message_buffer.update_agent_status("Market Analyst", "completed")
|
|
# Set next analyst to in_progress
|
|
if "social" in selections["analysts"]:
|
|
message_buffer.update_agent_status("Social Analyst", "in_progress")
|
|
|
|
if "sentiment_report" in chunk and chunk["sentiment_report"]:
|
|
message_buffer.update_report_section(
|
|
"sentiment_report", chunk["sentiment_report"]
|
|
)
|
|
message_buffer.update_agent_status("Social Analyst", "completed")
|
|
# Set next analyst to in_progress
|
|
if "news" in selections["analysts"]:
|
|
message_buffer.update_agent_status("News Analyst", "in_progress")
|
|
|
|
if "news_report" in chunk and chunk["news_report"]:
|
|
message_buffer.update_report_section("news_report", chunk["news_report"])
|
|
message_buffer.update_agent_status("News Analyst", "completed")
|
|
# Set next analyst to in_progress
|
|
if "fundamentals" in selections["analysts"]:
|
|
message_buffer.update_agent_status("Fundamentals Analyst", "in_progress")
|
|
|
|
if "fundamentals_report" in chunk and chunk["fundamentals_report"]:
|
|
message_buffer.update_report_section(
|
|
"fundamentals_report", chunk["fundamentals_report"]
|
|
)
|
|
message_buffer.update_agent_status("Fundamentals Analyst", "completed")
|
|
# Set all research team members to in_progress
|
|
update_research_team_status("in_progress")
|
|
|
|
# Research Team - Handle Investment Debate State
|
|
if "investment_debate_state" in chunk and chunk["investment_debate_state"]:
|
|
debate_state = chunk["investment_debate_state"]
|
|
|
|
# Update Bull Researcher status and report
|
|
if "bull_history" in debate_state and debate_state["bull_history"]:
|
|
# Keep all research team members in progress
|
|
update_research_team_status("in_progress")
|
|
# Extract latest bull response
|
|
bull_responses = debate_state["bull_history"].split("\n")
|
|
latest_bull = bull_responses[-1] if bull_responses else ""
|
|
if latest_bull:
|
|
message_buffer.add_message("Reasoning", latest_bull)
|
|
# Update research report with bull's latest analysis
|
|
message_buffer.update_report_section(
|
|
"investment_plan",
|
|
f"### Bull Researcher Analysis\n{latest_bull}",
|
|
)
|
|
|
|
# Update Bear Researcher status and report
|
|
if "bear_history" in debate_state and debate_state["bear_history"]:
|
|
# Keep all research team members in progress
|
|
update_research_team_status("in_progress")
|
|
# Extract latest bear response
|
|
bear_responses = debate_state["bear_history"].split("\n")
|
|
latest_bear = bear_responses[-1] if bear_responses else ""
|
|
if latest_bear:
|
|
message_buffer.add_message("Reasoning", latest_bear)
|
|
# Update research report with bear's latest analysis
|
|
message_buffer.update_report_section(
|
|
"investment_plan",
|
|
f"{message_buffer.report_sections['investment_plan']}\n\n### Bear Researcher Analysis\n{latest_bear}",
|
|
)
|
|
|
|
# Update Research Manager status and final decision
|
|
if "judge_decision" in debate_state and debate_state["judge_decision"]:
|
|
# Keep all research team members in progress until final decision
|
|
update_research_team_status("in_progress")
|
|
message_buffer.add_message(
|
|
"Reasoning",
|
|
f"Research Manager: {debate_state['judge_decision']}",
|
|
)
|
|
# Update research report with final decision
|
|
message_buffer.update_report_section(
|
|
"investment_plan",
|
|
f"{message_buffer.report_sections['investment_plan']}\n\n### Research Manager Decision\n{debate_state['judge_decision']}",
|
|
)
|
|
# Mark all research team members as completed
|
|
update_research_team_status("completed")
|
|
# Set first risk analyst to in_progress
|
|
message_buffer.update_agent_status("Risky Analyst", "in_progress")
|
|
|
|
# Trading Team
|
|
if "trader_investment_plan" in chunk and chunk["trader_investment_plan"]:
|
|
message_buffer.update_report_section(
|
|
"trader_investment_plan", chunk["trader_investment_plan"]
|
|
)
|
|
# Set first risk analyst to in_progress
|
|
message_buffer.update_agent_status("Risky Analyst", "in_progress")
|
|
|
|
# Risk Management Team - Handle Risk Debate State
|
|
if "risk_debate_state" in chunk and chunk["risk_debate_state"]:
|
|
risk_state = chunk["risk_debate_state"]
|
|
|
|
# Update Risky Analyst status and report
|
|
if (
|
|
"current_risky_response" in risk_state
|
|
and risk_state["current_risky_response"]
|
|
):
|
|
message_buffer.update_agent_status("Risky Analyst", "in_progress")
|
|
message_buffer.add_message(
|
|
"Reasoning",
|
|
f"Risky Analyst: {risk_state['current_risky_response']}",
|
|
)
|
|
# Update risk report with risky analyst's latest analysis only
|
|
message_buffer.update_report_section(
|
|
"final_trade_decision",
|
|
f"### Risky Analyst Analysis\n{risk_state['current_risky_response']}",
|
|
)
|
|
|
|
# Update Safe Analyst status and report
|
|
if (
|
|
"current_safe_response" in risk_state
|
|
and risk_state["current_safe_response"]
|
|
):
|
|
message_buffer.update_agent_status("Safe Analyst", "in_progress")
|
|
message_buffer.add_message(
|
|
"Reasoning",
|
|
f"Safe Analyst: {risk_state['current_safe_response']}",
|
|
)
|
|
# Update risk report with safe analyst's latest analysis only
|
|
message_buffer.update_report_section(
|
|
"final_trade_decision",
|
|
f"### Safe Analyst Analysis\n{risk_state['current_safe_response']}",
|
|
)
|
|
|
|
# Update Neutral Analyst status and report
|
|
if (
|
|
"current_neutral_response" in risk_state
|
|
and risk_state["current_neutral_response"]
|
|
):
|
|
message_buffer.update_agent_status("Neutral Analyst", "in_progress")
|
|
message_buffer.add_message(
|
|
"Reasoning",
|
|
f"Neutral Analyst: {risk_state['current_neutral_response']}",
|
|
)
|
|
# Update risk report with neutral analyst's latest analysis only
|
|
message_buffer.update_report_section(
|
|
"final_trade_decision",
|
|
f"### Neutral Analyst Analysis\n{risk_state['current_neutral_response']}",
|
|
)
|
|
|
|
# Update Portfolio Manager status and final decision
|
|
if "judge_decision" in risk_state and risk_state["judge_decision"]:
|
|
message_buffer.update_agent_status("Portfolio Manager", "in_progress")
|
|
message_buffer.add_message(
|
|
"Reasoning",
|
|
f"Portfolio Manager: {risk_state['judge_decision']}",
|
|
)
|
|
# Update risk report with final decision only
|
|
message_buffer.update_report_section(
|
|
"final_trade_decision",
|
|
f"### Final Trade Decision\n{risk_state['judge_decision']}",
|
|
)
|
|
# Mark risk analysts as completed
|
|
message_buffer.update_agent_status("Risky Analyst", "completed")
|
|
message_buffer.update_agent_status("Safe Analyst", "completed")
|
|
message_buffer.update_agent_status("Neutral Analyst", "completed")
|
|
message_buffer.update_agent_status("Portfolio Manager", "completed")
|
|
|
|
# Update the display
|
|
update_display(layout)
|
|
|
|
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(
|
|
"Analysis", 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])
|
|
|
|
# Display the complete final report
|
|
display_complete_report(final_state)
|
|
|
|
update_display(layout)
|
|
|
|
|
|
@app.command()
|
|
def build_memories(
|
|
start_date: str = typer.Option(
|
|
"2023-01-01", "--start-date", "-s", help="Start date for scanning high movers (YYYY-MM-DD)"
|
|
),
|
|
end_date: str = typer.Option(
|
|
"2024-12-01", "--end-date", "-e", help="End date for scanning high movers (YYYY-MM-DD)"
|
|
),
|
|
tickers: str = typer.Option(
|
|
None,
|
|
"--tickers",
|
|
"-t",
|
|
help="Comma-separated list of tickers to scan (overrides --use-alpha-vantage)",
|
|
),
|
|
use_alpha_vantage: bool = typer.Option(
|
|
False,
|
|
"--use-alpha-vantage",
|
|
"-a",
|
|
help="Use Alpha Vantage top gainers/losers to get ticker list",
|
|
),
|
|
av_limit: int = typer.Option(
|
|
20,
|
|
"--av-limit",
|
|
help="Number of tickers to get from each Alpha Vantage category (gainers/losers)",
|
|
),
|
|
min_move_pct: float = typer.Option(
|
|
15.0, "--min-move", "-m", help="Minimum percentage move to qualify as high mover"
|
|
),
|
|
analysis_windows: str = typer.Option(
|
|
"7,30",
|
|
"--windows",
|
|
"-w",
|
|
help="Comma-separated list of days before move to analyze (e.g., '7,30')",
|
|
),
|
|
max_samples: int = typer.Option(
|
|
20, "--max-samples", help="Maximum number of high movers to analyze (reduces runtime)"
|
|
),
|
|
sample_strategy: str = typer.Option(
|
|
"diverse", "--strategy", help="Sampling strategy: diverse, largest, recent, or random"
|
|
),
|
|
):
|
|
"""
|
|
Build historical memories from high movers.
|
|
|
|
This command:
|
|
1. Scans for stocks with significant moves (>15% in 5 days by default)
|
|
2. Runs retrospective trading analyses at T-7 and T-30 days before the move
|
|
3. Stores situations, outcomes, and agent correctness in ChromaDB
|
|
4. Creates a memory bank for future trading decisions
|
|
|
|
Examples:
|
|
# Use Alpha Vantage top movers
|
|
python cli/main.py build-memories --use-alpha-vantage
|
|
|
|
# Use specific tickers
|
|
python cli/main.py build-memories --tickers "AAPL,NVDA,TSLA"
|
|
|
|
# Customize date range and parameters
|
|
python cli/main.py build-memories --use-alpha-vantage --start-date 2023-01-01 --min-move 20.0
|
|
"""
|
|
console.print(
|
|
"\n[bold cyan]═══════════════════════════════════════════════════════[/bold cyan]"
|
|
)
|
|
console.print("[bold cyan] TRADINGAGENTS MEMORY BUILDER[/bold cyan]")
|
|
console.print(
|
|
"[bold cyan]═══════════════════════════════════════════════════════[/bold cyan]\n"
|
|
)
|
|
|
|
# Determine ticker source
|
|
if use_alpha_vantage and not tickers:
|
|
console.print("[bold yellow]📡 Using Alpha Vantage to fetch top movers...[/bold yellow]")
|
|
try:
|
|
from tradingagents.agents.utils.historical_memory_builder import HistoricalMemoryBuilder
|
|
|
|
builder_temp = HistoricalMemoryBuilder(DEFAULT_CONFIG)
|
|
ticker_list = builder_temp.get_tickers_from_alpha_vantage(limit=av_limit)
|
|
|
|
if not ticker_list:
|
|
console.print(
|
|
"\n[bold red]❌ No tickers found from Alpha Vantage. Please check your API key or try --tickers instead.[/bold red]\n"
|
|
)
|
|
raise typer.Exit(code=1)
|
|
except Exception as e:
|
|
console.print(f"\n[bold red]❌ Error fetching from Alpha Vantage: {e}[/bold red]")
|
|
console.print("[yellow]Please use --tickers to specify tickers manually.[/yellow]\n")
|
|
raise typer.Exit(code=1)
|
|
elif tickers:
|
|
ticker_list = [t.strip().upper() for t in tickers.split(",")]
|
|
console.print(f"[bold]Using {len(ticker_list)} specified tickers[/bold]")
|
|
else:
|
|
# Default tickers if neither option specified
|
|
default_tickers = "AAPL,MSFT,GOOGL,NVDA,TSLA,META,AMZN,AMD,NFLX,DIS"
|
|
ticker_list = [t.strip().upper() for t in default_tickers.split(",")]
|
|
console.print("[bold yellow]No ticker source specified. Using default list.[/bold yellow]")
|
|
console.print(
|
|
"[dim]Tip: Use --use-alpha-vantage for dynamic ticker discovery or --tickers for custom list[/dim]"
|
|
)
|
|
|
|
window_list = [int(w.strip()) for w in analysis_windows.split(",")]
|
|
|
|
console.print("\n[bold]Configuration:[/bold]")
|
|
console.print(f" Ticker Source: {'Alpha Vantage' if use_alpha_vantage else 'Manual/Default'}")
|
|
console.print(f" Date Range: {start_date} to {end_date}")
|
|
console.print(f" Tickers: {len(ticker_list)} stocks")
|
|
console.print(f" Min Move: {min_move_pct}%")
|
|
console.print(f" Max Samples: {max_samples}")
|
|
console.print(f" Sampling Strategy: {sample_strategy}")
|
|
console.print(f" Analysis Windows: {window_list} days before move")
|
|
console.print()
|
|
|
|
try:
|
|
# Import here to avoid circular imports
|
|
from tradingagents.agents.utils.historical_memory_builder import HistoricalMemoryBuilder
|
|
|
|
# Create builder
|
|
builder = HistoricalMemoryBuilder(DEFAULT_CONFIG)
|
|
|
|
# Build memories
|
|
memories = builder.build_memories_from_high_movers(
|
|
tickers=ticker_list,
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
min_move_pct=min_move_pct,
|
|
analysis_windows=window_list,
|
|
max_samples=max_samples,
|
|
sample_strategy=sample_strategy,
|
|
)
|
|
|
|
if not memories:
|
|
console.print(
|
|
"\n[bold yellow]⚠️ No memories created. Try adjusting parameters.[/bold yellow]\n"
|
|
)
|
|
return
|
|
|
|
# Display summary table
|
|
console.print("\n[bold green]✅ Memory building complete![/bold green]\n")
|
|
|
|
table = Table(title="Memory Bank Summary", box=box.ROUNDED)
|
|
table.add_column("Agent Type", style="cyan", no_wrap=True)
|
|
table.add_column("Total Memories", justify="right", style="magenta")
|
|
table.add_column("Accuracy Rate", justify="right", style="green")
|
|
table.add_column("Avg Move %", justify="right", style="yellow")
|
|
|
|
for agent_type, memory in memories.items():
|
|
stats = memory.get_statistics()
|
|
table.add_row(
|
|
agent_type.upper(),
|
|
str(stats["total_memories"]),
|
|
f"{stats['accuracy_rate']:.1f}%",
|
|
f"{stats['avg_move_pct']:.1f}%",
|
|
)
|
|
|
|
console.print(table)
|
|
console.print()
|
|
|
|
# Test memory retrieval
|
|
console.print("[bold]Testing Memory Retrieval:[/bold]")
|
|
test_situation = """
|
|
Strong earnings beat with positive sentiment and bullish technical indicators.
|
|
Volume spike detected. Analyst upgrades present. News sentiment is positive.
|
|
"""
|
|
|
|
console.print(f" Query: '{test_situation.strip()[:100]}...'\n")
|
|
|
|
for agent_type, memory in list(memories.items())[:2]: # Test first 2 agents
|
|
results = memory.get_memories(test_situation, n_matches=1)
|
|
if results:
|
|
console.print(
|
|
f" [cyan]{agent_type.upper()}[/cyan]: Found {len(results)} relevant memory"
|
|
)
|
|
console.print(f" Similarity: {results[0]['similarity_score']:.2f}")
|
|
|
|
console.print("\n[bold green]🎉 Memory bank ready for use![/bold green]")
|
|
console.print(
|
|
"\n[dim]Note: These memories will be used automatically in future trading analyses when memory is enabled in config.[/dim]\n"
|
|
)
|
|
|
|
except Exception as e:
|
|
console.print("\n[bold red]❌ Error building memories:[/bold red]")
|
|
console.print(f"[red]{str(e)}[/red]\n")
|
|
import traceback
|
|
|
|
console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
raise typer.Exit(code=1)
|
|
|
|
|
|
@app.command()
|
|
def analyze():
|
|
run_analysis()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app()
|