TradingAgents/cli/main.py

1117 lines
44 KiB
Python

from typing import Optional
import datetime
import re
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 cli.announcements import fetch_announcements, display_announcements
from cli.stats_handler import StatsCallbackHandler
console = Console()
app = typer.Typer(
name="TradingAgents",
help="TradingAgents CLI: Swing Trading with Multi-Agent LLM Framework",
add_completion=True,
)
class MessageBuffer:
"""Tracks agent status and reports during swing trading analysis."""
# Fixed agents (always run after analysts)
FIXED_AGENTS = {
"Trading Team": ["Trader"],
}
# Analyst name mapping
ANALYST_MAPPING = {
"market": "Market Analyst",
"news": "News Analyst",
"fundamentals": "Fundamentals Analyst",
}
# Report sections: section -> (analyst_key for filtering, finalizing_agent)
REPORT_SECTIONS = {
"market_report": ("market", "Market Analyst"),
"news_report": ("news", "News Analyst"),
"fundamentals_report": ("fundamentals", "Fundamentals Analyst"),
"trader_decision": (None, "Trader"),
}
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
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."""
self.selected_analysts = [a.lower() for a in selected_analysts]
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 agents
for team_agents in self.FIXED_AGENTS.values():
for agent in team_agents:
self.agent_status[agent] = "pending"
# Build report_sections
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
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 = 0
for section in self.report_sections:
if section not in self.REPORT_SECTIONS:
continue
_, finalizing_agent = self.REPORT_SECTIONS[section]
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):
latest_section = None
latest_content = None
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:
section_titles = {
"market_report": "Market Analysis (기술적 분석)",
"news_report": "News Analysis (뉴스 분석)",
"fundamentals_report": "Fundamentals Analysis (기본적 분석)",
"trader_decision": "Swing Trading Decision (매매 결정)",
}
title = section_titles.get(latest_section, latest_section)
self.current_report = f"### {title}\n{latest_content}"
self._update_final_report()
def _update_final_report(self):
report_parts = []
analyst_sections = ["market_report", "news_report", "fundamentals_report"]
if any(self.report_sections.get(section) for section in analyst_sections):
report_parts.append("## Analyst 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("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']}"
)
if self.report_sections.get("trader_decision"):
report_parts.append("## Swing Trading Decision")
report_parts.append(f"{self.report_sections['trader_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):
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
layout["header"].update(
Panel(
"[bold green]TradingAgents - Swing Trading[/bold green]\n"
"[dim]Analysts \u2192 Trader \u2192 Decision[/dim]",
title="Swing Trading Pipeline",
border_style="green",
padding=(1, 2),
expand=True,
)
)
# Progress panel
progress_table = Table(
show_header=True,
header_style="bold magenta",
show_footer=False,
box=box.SIMPLE_HEAD,
padding=(0, 2),
expand=True,
)
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)
all_teams = {
"Analyst Team": ["Market Analyst", "News Analyst", "Fundamentals Analyst"],
"Trading Team": ["Trader"],
}
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():
first_agent = agents[0]
status = message_buffer.agent_status.get(first_agent, "pending")
if status == "in_progress":
status_cell = Spinner("dots", text="[blue]in_progress[/blue]", style="bold cyan")
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)
for agent in agents[1:]:
status = message_buffer.agent_status.get(agent, "pending")
if status == "in_progress":
status_cell = Spinner("dots", text="[blue]in_progress[/blue]", style="bold cyan")
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)
progress_table.add_row("\u2500" * 20, "\u2500" * 20, "\u2500" * 20, style="dim")
layout["progress"].update(
Panel(progress_table, title="Progress", border_style="cyan", padding=(1, 2))
)
# Messages panel
messages_table = Table(
show_header=True,
header_style="bold magenta",
show_footer=False,
expand=True,
box=box.MINIMAL,
show_lines=True,
padding=(0, 1),
)
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)
all_messages = []
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}"))
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))
all_messages.sort(key=lambda x: x[0], reverse=True)
for timestamp, msg_type, content in all_messages[:12]:
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
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...[/italic]", title="Current Report", border_style="green", padding=(1, 2))
)
# Footer
agents_completed = sum(1 for s in message_buffer.agent_status.values() if s == "completed")
agents_total = len(message_buffer.agent_status)
reports_completed = message_buffer.get_completed_reports_count()
reports_total = len(message_buffer.report_sections)
stats_parts = [f"Agents: {agents_completed}/{agents_total}"]
if stats_handler:
stats = stats_handler.get_stats()
stats_parts.append(f"LLM: {stats['llm_calls']}")
stats_parts.append(f"Tools: {stats['tool_calls']}")
if stats["tokens_in"] > 0 or stats["tokens_out"] > 0:
stats_parts.append(f"Tokens: {format_tokens(stats['tokens_in'])}\u2191 {format_tokens(stats['tokens_out'])}\u2193")
else:
stats_parts.append("Tokens: --")
stats_parts.append(f"Reports: {reports_completed}/{reports_total}")
if start_time:
elapsed = time.time() - start_time
stats_parts.append(f"\u23f1 {int(elapsed // 60):02d}:{int(elapsed % 60):02d}")
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 get_user_selections():
"""Get all user selections before starting analysis."""
# Display welcome
try:
with open("./cli/static/welcome.txt", "r") as f:
welcome_ascii = f.read()
except FileNotFoundError:
welcome_ascii = ""
welcome_content = f"{welcome_ascii}\n"
welcome_content += "[bold green]TradingAgents: Swing Trading Framework[/bold green]\n\n"
welcome_content += "[bold]Pipeline:[/bold] Analysts \u2192 Trader \u2192 Swing Decision\n\n"
welcome_content += "[dim]Built by [Tauric Research](https://github.com/TauricResearch)[/dim]"
welcome_box = Panel(
welcome_content,
border_style="green",
padding=(1, 2),
title="Swing Trading Pipeline",
)
console.print(Align.center(welcome_box))
console.print()
announcements = fetch_announcements()
display_announcements(console, announcements)
def create_question_box(title, prompt, default=None):
box_content = f"[bold]{title}[/bold]\n[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
console.print(create_question_box("Step 1: Ticker Symbol", "Enter the ticker symbol to analyze", "SPY"))
selected_ticker = get_ticker()
# Step 2: 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: Analysts
console.print(create_question_box("Step 3: Analysts", "Select analysts for the analysis"))
selected_analysts = select_analysts()
console.print(f"[green]Selected:[/green] {', '.join(a.value for a in selected_analysts)}")
# Step 4: LLM provider
console.print(create_question_box("Step 4: LLM Provider", "Select which LLM service to use"))
selected_llm_provider, backend_url = select_llm_provider()
# Step 5: LLM models
console.print(create_question_box("Step 5: LLM Models", "Select your thinking agents"))
selected_shallow_thinker = select_shallow_thinking_agent(selected_llm_provider)
selected_deep_thinker = select_deep_thinking_agent(selected_llm_provider)
# Step 6: Provider-specific config
thinking_level = None
reasoning_effort = None
provider_lower = selected_llm_provider.lower()
if provider_lower == "google":
console.print(create_question_box("Step 6: Thinking Mode", "Configure Gemini thinking mode"))
thinking_level = ask_gemini_thinking_config()
elif provider_lower == "openai":
console.print(create_question_box("Step 6: Reasoning Effort", "Configure OpenAI reasoning effort"))
reasoning_effort = ask_openai_reasoning_effort()
return {
"ticker": selected_ticker,
"analysis_date": analysis_date,
"analysts": selected_analysts,
"llm_provider": selected_llm_provider.lower(),
"backend_url": backend_url,
"shallow_thinker": selected_shallow_thinker,
"deep_thinker": selected_deep_thinker,
"google_thinking_level": thinking_level,
"openai_reasoning_effort": reasoning_effort,
}
def save_report_to_disk(final_state, ticker: str, save_path: Path):
"""Save swing trading report to disk."""
save_path.mkdir(parents=True, exist_ok=True)
sections = []
# 1. Analyst reports
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("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 Reports\n\n{content}")
# 2. Trader decision
if final_state.get("trader_decision"):
trading_dir = save_path / "2_trading"
trading_dir.mkdir(exist_ok=True)
(trading_dir / "trader.md").write_text(final_state["trader_decision"])
sections.append(f"## II. Swing Trading Decision\n\n### Trader\n{final_state['trader_decision']}")
# Write consolidated report
header = f"# Swing Trading 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 translate_report_to_korean(report_content: str, llm) -> str:
"""영문 트레이딩 분석 리포트를 한국어로 번역."""
from langchain_core.messages import HumanMessage, SystemMessage
system_prompt = (
"당신은 금융 전문가이자 비전공자 교육 전문가입니다.\n"
"영어 주식 트레이딩 분석 리포트를 한국어로 번역하고, "
"금융·기술 전문 용어를 비전공자도 쉽게 이해할 수 있도록 설명을 추가해주세요.\n\n"
"번역 지침:\n"
"1. 자연스러운 한국어로 번역하세요.\n"
"2. 처음 등장하는 전문 용어 뒤에 괄호로 쉬운 설명을 추가하세요.\n"
"3. 복잡한 분석 개념은 일상적인 비유를 사용해 쉽게 설명하세요.\n"
"4. 가격, 퍼센트 등 수치와 종목 코드는 그대로 유지하세요.\n"
"5. 마크다운 형식을 그대로 유지하세요.\n"
"6. 섹션 제목은 한국어로 번역하세요.\n"
"7. 최종 투자 의견과 권고 사항을 명확히 전달하세요."
)
parts = re.split(r"(?=^## )", report_content, flags=re.MULTILINE)
translated_parts = []
for part in parts:
if not part.strip():
continue
response = llm.invoke([
SystemMessage(content=system_prompt),
HumanMessage(content=f"아래 내용을 한국어로 번역하고 전문 용어를 쉽게 설명해주세요:\n\n{part}"),
])
translated_parts.append(response.content)
return "\n\n".join(translated_parts)
def display_complete_report(final_state):
"""Display the complete analysis report."""
console.print()
console.print(Rule("Swing Trading Report", style="bold green"))
# Analyst Reports
analysts = []
if final_state.get("market_report"):
analysts.append(("Market Analyst", final_state["market_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 Reports[/bold]", border_style="cyan"))
for title, content in analysts:
console.print(Panel(Markdown(content), title=title, border_style="blue", padding=(1, 2)))
# Trader Decision
if final_state.get("trader_decision"):
console.print(Panel("[bold]II. Swing Trading Decision[/bold]", border_style="yellow"))
console.print(Panel(Markdown(final_state["trader_decision"]), title="Trader", border_style="blue", padding=(1, 2)))
# Ordered list of analysts for status transitions
ANALYST_ORDER = ["market", "news", "fundamentals"]
ANALYST_AGENT_NAMES = {
"market": "Market Analyst",
"news": "News Analyst",
"fundamentals": "Fundamentals Analyst",
}
ANALYST_REPORT_MAP = {
"market": "market_report",
"news": "news_report",
"fundamentals": "fundamentals_report",
}
def update_analyst_statuses(message_buffer, chunk):
"""Update analyst statuses based on current report state."""
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 done, set Trader to in_progress
if not found_active and selected:
if message_buffer.agent_status.get("Trader") == "pending":
message_buffer.update_agent_status("Trader", "in_progress")
def extract_content_string(content):
"""Extract string content from various message formats."""
import ast
def is_empty(val):
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
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."""
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)
return ("System", content)
def format_tool_args(args, max_length=80) -> str:
result = str(args)
if len(result) > max_length:
return result[:max_length - 3] + "..."
return result
def run_analysis():
selections = get_user_selections()
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()
config["google_thinking_level"] = selections.get("google_thinking_level")
config["openai_reasoning_effort"] = selections.get("openai_reasoning_effort")
stats_handler = StatsCallbackHandler()
# Normalize analyst selection to predefined order
selected_set = {analyst.value for analyst in selections["analysts"]}
selected_analyst_keys = [a for a in ANALYST_ORDER if a in selected_set]
graph = TradingAgentsGraph(
selected_analyst_keys,
config=config,
debug=True,
callbacks=[stats_handler],
)
message_buffer.init_for_analysis(selected_analyst_keys)
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", " ")
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:
with open(report_dir / f"{section_name}.md", "w") 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")
layout = create_layout()
with Live(layout, refresh_per_second=4) as live:
update_display(layout, stats_handler=stats_handler, start_time=start_time)
message_buffer.add_message("System", f"Ticker: {selections['ticker']}")
message_buffer.add_message("System", f"Date: {selections['analysis_date']}")
message_buffer.add_message("System", f"Analysts: {', '.join(a.value for a in selections['analysts'])}")
update_display(layout, stats_handler=stats_handler, start_time=start_time)
# Set first analyst to in_progress
first_analyst_key = selected_analyst_keys[0]
first_analyst_name = ANALYST_AGENT_NAMES[first_analyst_key]
message_buffer.update_agent_status(first_analyst_name, "in_progress")
update_display(layout, stats_handler=stats_handler, start_time=start_time)
# Initialize state and stream
init_state = graph.propagator.create_initial_state(
selections["ticker"], selections["analysis_date"]
)
args = graph.propagator.get_graph_args(callbacks=[stats_handler])
trace = []
for chunk in graph.graph.stream(init_state, **args):
# Process messages
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
msg_type, content = classify_message_type(last_message)
if content and content.strip():
message_buffer.add_message(msg_type, content)
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
update_analyst_statuses(message_buffer, chunk)
# Trader decision
if chunk.get("trader_decision"):
message_buffer.update_report_section("trader_decision", chunk["trader_decision"])
if message_buffer.agent_status.get("Trader") != "completed":
message_buffer.update_agent_status("Trader", "completed")
update_display(layout, stats_handler=stats_handler, start_time=start_time)
trace.append(chunk)
# Get final state
final_state = trace[-1]
# Update all agents to completed
for agent in message_buffer.agent_status:
message_buffer.update_agent_status(agent, "completed")
message_buffer.add_message("System", f"Analysis complete for {selections['ticker']}")
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
console.print("\n[bold cyan]Analysis Complete![/bold cyan]\n")
# Process swing signal
swing_signal = graph.process_signal(final_state.get("trader_decision", ""))
action = swing_signal.get("action", "PASS")
console.print(f"[bold]Swing Decision:[/bold] [{'green' if action == 'BUY' else 'yellow' if action == 'SELL' else 'dim'}]{action}[/]")
if action != "PASS":
if swing_signal.get("entry_price"):
console.print(f" Entry: {swing_signal['entry_price']}")
if swing_signal.get("stop_loss"):
console.print(f" Stop Loss: {swing_signal['stop_loss']}")
if swing_signal.get("take_profit"):
console.print(f" Take Profit: {swing_signal['take_profit']}")
if swing_signal.get("position_size_pct"):
console.print(f" Position Size: {swing_signal['position_size_pct']*100:.0f}%")
if swing_signal.get("max_hold_days"):
console.print(f" Max Hold: {swing_signal['max_hold_days']} days")
# Save report
save_choice = typer.prompt("\nSave 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 (Enter for default)", default=str(default_path)).strip()
save_path = Path(save_path_str)
report_file = None
try:
report_file = save_report_to_disk(final_state, selections["ticker"], save_path)
console.print(f"\n[green]\u2713 Report saved:[/green] {save_path.resolve()}")
except Exception as e:
console.print(f"[red]Error saving report: {e}[/red]")
# Korean translation
if report_file and report_file.exists():
ko_choice = typer.prompt("\n한국어 번역 리포트 생성?", default="Y").strip().upper()
if ko_choice in ("Y", "YES", ""):
console.print("\n[bold cyan]한국어로 번역 중...[/bold cyan]")
try:
korean_content = translate_report_to_korean(
report_file.read_text(), graph.deep_thinking_llm
)
ko_header = (
f"# 스윙 트레이딩 리포트: {selections['ticker']} (한국어)\n\n"
f"생성: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n---\n\n"
)
ko_file = save_path / "complete_report_ko.md"
ko_file.write_text(ko_header + korean_content)
console.print(f"[green]\u2713 한국어 번역 완료:[/green] {ko_file.name}")
except Exception as e:
console.print(f"[red]번역 오류: {e}[/red]")
# Display full report
display_choice = typer.prompt("\nDisplay full report?", default="Y").strip().upper()
if display_choice in ("Y", "YES", ""):
display_complete_report(final_state)
@app.command()
def analyze():
"""Analyze a single ticker (manual input)."""
run_analysis()
@app.command()
def swing():
"""Full swing trading pipeline: Screen stocks → Analyze candidates → Trading decisions."""
run_swing_pipeline()
def _get_swing_config():
"""Get config selections for swing pipeline (no ticker needed)."""
try:
with open("./cli/static/welcome.txt", "r") as f:
welcome_ascii = f.read()
except FileNotFoundError:
welcome_ascii = ""
welcome_content = f"{welcome_ascii}\n"
welcome_content += "[bold green]TradingAgents: Swing Trading Pipeline[/bold green]\n\n"
welcome_content += "[bold]Pipeline:[/bold] Screening → Analysts → Trader → Swing Decision\n\n"
welcome_content += "[dim]Built by [Tauric Research](https://github.com/TauricResearch)[/dim]"
console.print(Align.center(Panel(
welcome_content, border_style="green", padding=(1, 2), title="Swing Trading Pipeline",
)))
console.print()
announcements = fetch_announcements()
display_announcements(console, announcements)
def create_question_box(title, prompt, default=None):
box_content = f"[bold]{title}[/bold]\n[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: Date
default_date = datetime.datetime.now().strftime("%Y-%m-%d")
console.print(create_question_box("Step 1: Trading Date", "Enter the trading date", default_date))
analysis_date = get_analysis_date()
# Step 2: Market
import questionary
console.print(create_question_box("Step 2: Market", "Select target market"))
market_choice = questionary.select(
"Select Market:",
choices=["KRX (한국)", "US (미국)"],
style=questionary.Style([("selected", "fg:green noinherit"), ("highlighted", "fg:green noinherit")]),
).ask()
market = "KRX" if "KRX" in (market_choice or "KRX") else "US"
# Step 3: Analysts
console.print(create_question_box("Step 3: Analysts", "Select analysts for candidate analysis"))
selected_analysts = select_analysts()
# Step 4: LLM
console.print(create_question_box("Step 4: LLM Provider", "Select LLM service"))
selected_llm_provider, backend_url = select_llm_provider()
# Step 5: Models
console.print(create_question_box("Step 5: LLM Models", "Select thinking agents"))
selected_shallow_thinker = select_shallow_thinking_agent(selected_llm_provider)
selected_deep_thinker = select_deep_thinking_agent(selected_llm_provider)
# Step 6: Provider config
thinking_level = None
reasoning_effort = None
provider_lower = selected_llm_provider.lower()
if provider_lower == "google":
console.print(create_question_box("Step 6: Thinking Mode", "Configure Gemini"))
thinking_level = ask_gemini_thinking_config()
elif provider_lower == "openai":
console.print(create_question_box("Step 6: Reasoning Effort", "Configure OpenAI"))
reasoning_effort = ask_openai_reasoning_effort()
return {
"analysis_date": analysis_date,
"market": market,
"analysts": selected_analysts,
"llm_provider": selected_llm_provider.lower(),
"backend_url": backend_url,
"shallow_thinker": selected_shallow_thinker,
"deep_thinker": selected_deep_thinker,
"google_thinking_level": thinking_level,
"openai_reasoning_effort": reasoning_effort,
}
def _display_swing_signal(ticker: str, name: str, swing_signal: dict):
"""Display a single swing signal result."""
action = swing_signal.get("action", "PASS")
color = {"BUY": "green", "SELL": "yellow", "HOLD": "cyan"}.get(action, "dim")
parts = [f"[bold][{color}]{action}[/{color}][/bold] {name} ({ticker})"]
if action != "PASS":
details = []
if swing_signal.get("entry_price"):
details.append(f"진입가: {swing_signal['entry_price']}")
if swing_signal.get("stop_loss"):
details.append(f"손절: {swing_signal['stop_loss']}")
if swing_signal.get("take_profit"):
details.append(f"익절: {swing_signal['take_profit']}")
if swing_signal.get("position_size_pct"):
details.append(f"비중: {swing_signal['position_size_pct']*100:.0f}%")
if swing_signal.get("max_hold_days"):
details.append(f"보유: {swing_signal['max_hold_days']}")
if details:
parts.append(" " + " | ".join(details))
if swing_signal.get("rationale"):
parts.append(f" 사유: {swing_signal['rationale']}")
for part in parts:
console.print(part)
def run_swing_pipeline():
"""Run full swing pipeline: screen → analyze → decide."""
selections = _get_swing_config()
config = DEFAULT_CONFIG.copy()
config["market"] = selections["market"]
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"]
config["google_thinking_level"] = selections.get("google_thinking_level")
config["openai_reasoning_effort"] = selections.get("openai_reasoning_effort")
selected_set = {a.value for a in selections["analysts"]}
selected_analyst_keys = [a for a in ANALYST_ORDER if a in selected_set]
stats_handler = StatsCallbackHandler()
graph = TradingAgentsGraph(
selected_analyst_keys,
config=config,
debug=False,
callbacks=[stats_handler],
)
trade_date = selections["analysis_date"]
# ─── Phase 1: Screening ───
console.print()
console.print(Rule("Phase 1: Stock Screening (종목 발굴)", style="bold cyan"))
console.print(f"[dim]Market: {selections['market']} / Date: {trade_date}[/dim]\n")
with console.status("[bold cyan]Scanning market universe...[/bold cyan]"):
screening_result = graph.screen(trade_date=trade_date)
# Display screening report
console.print(Panel(
screening_result.get("report", "No report"),
title="Screening Report",
border_style="cyan",
padding=(1, 2),
))
candidates = screening_result.get("candidates", [])
stats = screening_result.get("stats", {})
console.print(
f"\n[bold]Results:[/bold] {stats.get('universe_size', 0)} universe → "
f"{stats.get('technical_passed', 0)} technical → "
f"{stats.get('fundamental_passed', 0)} fundamental → "
f"[bold green]{stats.get('final_selected', 0)} final candidates[/bold green]"
)
if not candidates:
console.print("\n[yellow]No candidates found. Try adjusting screening criteria.[/yellow]")
return
# Ask to proceed
proceed = typer.prompt(
f"\n{len(candidates)}개 후보 종목을 분석하시겠습니까?", default="Y"
).strip().upper()
if proceed not in ("Y", "YES", ""):
return
# ─── Phase 2: Analyze each candidate ───
console.print()
console.print(Rule("Phase 2: Candidate Analysis (후보 분석)", style="bold yellow"))
all_results = []
for i, candidate in enumerate(candidates, 1):
ticker = candidate["ticker"]
name = candidate.get("name", ticker)
signals = ", ".join(candidate.get("signals", []))
screening_context = (
f"종목: {name} ({ticker})\n"
f"기술적 신호: {signals}\n"
f"펀더멘탈: {candidate.get('fundamental_check', 'N/A')}"
)
console.print(f"\n[bold]({i}/{len(candidates)}) {name} ({ticker})[/bold]")
console.print(f" [dim]{signals}[/dim]")
try:
with console.status(f"[bold cyan]Analyzing {ticker}...[/bold cyan]"):
final_state, swing_signal = graph.propagate(
company_name=ticker,
trade_date=trade_date,
screening_context=screening_context,
)
_display_swing_signal(ticker, name, swing_signal)
all_results.append({
"ticker": ticker,
"name": name,
"swing_signal": swing_signal,
"final_state": final_state,
"screening_context": screening_context,
})
except Exception as e:
console.print(f" [red]Analysis failed: {e}[/red]")
# ─── Phase 3: Summary ───
console.print()
console.print(Rule("Summary (종합 결과)", style="bold green"))
buy_signals = [r for r in all_results if r["swing_signal"].get("action") == "BUY"]
sell_signals = [r for r in all_results if r["swing_signal"].get("action") == "SELL"]
pass_signals = [r for r in all_results if r["swing_signal"].get("action") in ("PASS", "HOLD")]
# Summary table
summary_table = Table(
title="Swing Trading Signals",
show_header=True,
header_style="bold",
box=box.ROUNDED,
padding=(0, 1),
)
summary_table.add_column("Action", justify="center", width=8)
summary_table.add_column("Ticker", justify="center", width=10)
summary_table.add_column("Name", width=20)
summary_table.add_column("Entry", justify="right", width=12)
summary_table.add_column("Stop Loss", justify="right", width=12)
summary_table.add_column("Take Profit", justify="right", width=12)
summary_table.add_column("Size", justify="center", width=8)
summary_table.add_column("Hold", justify="center", width=8)
for r in all_results:
sig = r["swing_signal"]
action = sig.get("action", "PASS")
color = {"BUY": "green", "SELL": "yellow"}.get(action, "dim")
entry = str(sig.get("entry_price", "-"))
sl = str(sig.get("stop_loss", "-"))
tp = str(sig.get("take_profit", "-"))
size = f"{sig['position_size_pct']*100:.0f}%" if sig.get("position_size_pct") else "-"
hold = f"{sig['max_hold_days']}d" if sig.get("max_hold_days") else "-"
summary_table.add_row(
f"[{color}]{action}[/{color}]",
r["ticker"], r["name"],
entry, sl, tp, size, hold,
)
console.print(summary_table)
console.print(f"\n[bold green]BUY: {len(buy_signals)}[/bold green] / "
f"[bold yellow]SELL: {len(sell_signals)}[/bold yellow] / "
f"[dim]PASS: {len(pass_signals)}[/dim]")
# Save reports
if all_results:
save_choice = typer.prompt("\nSave reports?", default="Y").strip().upper()
if save_choice in ("Y", "YES", ""):
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
base_path = Path.cwd() / "reports" / f"swing_{selections['market']}_{timestamp}"
base_path.mkdir(parents=True, exist_ok=True)
for r in all_results:
try:
ticker_dir = base_path / r["ticker"]
save_report_to_disk(r["final_state"], r["ticker"], ticker_dir)
except Exception as e:
console.print(f"[red]Error saving {r['ticker']}: {e}[/red]")
# Save summary
summary_lines = [
f"# Swing Trading Summary: {selections['market']}",
f"Date: {trade_date}",
f"Generated: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
"",
]
for r in all_results:
sig = r["swing_signal"]
summary_lines.append(f"## {r['name']} ({r['ticker']}) - {sig.get('action', 'PASS')}")
if sig.get("rationale"):
summary_lines.append(f"사유: {sig['rationale']}")
summary_lines.append("")
(base_path / "summary.md").write_text("\n".join(summary_lines))
console.print(f"\n[green]\u2713 Reports saved:[/green] {base_path.resolve()}")
if __name__ == "__main__":
app()