perf: optimize redundant iteration in get_completed_reports_count

Co-authored-by: aguzererler <6199053+aguzererler@users.noreply.github.com>
This commit is contained in:
google-labs-jules[bot] 2026-03-21 19:51:52 +00:00
parent a7b8c996f2
commit f30a42ccab
1 changed files with 328 additions and 124 deletions

View File

@ -15,14 +15,12 @@ load_dotenv(Path(__file__).resolve().parent.parent / ".env")
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
@ -32,7 +30,6 @@ from tradingagents.report_paths import get_daily_dir, get_market_dir, get_ticker
from tradingagents.daily_digest import append_to_digest
from tradingagents.notebook_sync import sync_to_notebooklm
from tradingagents.default_config import DEFAULT_CONFIG
from cli.models import AnalystType
from cli.utils import *
from tradingagents.graph.scanner_graph import ScannerGraph
from cli.announcements import fetch_announcements, display_announcements
@ -54,7 +51,11 @@ class MessageBuffer:
FIXED_AGENTS = {
"Research Team": ["Bull Researcher", "Bear Researcher", "Research Manager"],
"Trading Team": ["Trader"],
"Risk Management": ["Aggressive Analyst", "Neutral Analyst", "Conservative Analyst"],
"Risk Management": [
"Aggressive Analyst",
"Neutral Analyst",
"Conservative Analyst",
],
"Portfolio Management": ["Portfolio Manager"],
}
@ -135,15 +136,10 @@ class MessageBuffer:
This prevents interim updates (like debate rounds) from counting as completed.
"""
count = 0
for section in self.report_sections:
if section not in self.REPORT_SECTIONS:
continue
_, finalizing_agent = self.REPORT_SECTIONS[section]
# Report is complete if it has content AND its finalizing agent is done
has_content = self.report_sections.get(section) is not None
agent_done = self.agent_status.get(finalizing_agent) == "completed"
if has_content and agent_done:
count += 1
for section, (_, finalizing_agent) in self.REPORT_SECTIONS.items():
if self.report_sections.get(section) is not None:
if self.agent_status.get(finalizing_agent) == "completed":
count += 1
return count
def add_message(self, message_type, content):
@ -174,7 +170,7 @@ class MessageBuffer:
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 = {
@ -197,7 +193,12 @@ class MessageBuffer:
report_parts = []
# Analyst Team Reports - use .get() to handle missing sections
analyst_sections = ["market_report", "sentiment_report", "news_report", "fundamentals_report"]
analyst_sections = [
"market_report",
"sentiment_report",
"news_report",
"fundamentals_report",
]
if any(self.report_sections.get(section) for section in analyst_sections):
report_parts.append("## Analyst Team Reports")
if self.report_sections.get("market_report"):
@ -257,7 +258,7 @@ def create_layout():
def format_tokens(n):
"""Format token count for display."""
if n >= 1000:
return f"{n/1000:.1f}k"
return f"{n / 1000:.1f}k"
return str(n)
@ -298,7 +299,11 @@ def update_display(layout, spinner_text=None, stats_handler=None, start_time=Non
],
"Research Team": ["Bull Researcher", "Bear Researcher", "Research Manager"],
"Trading Team": ["Trader"],
"Risk Management": ["Aggressive Analyst", "Neutral Analyst", "Conservative Analyst"],
"Risk Management": [
"Aggressive Analyst",
"Neutral Analyst",
"Conservative Analyst",
],
"Portfolio Management": ["Portfolio Manager"],
}
@ -559,34 +564,40 @@ def get_user_selections():
console.print(
create_question_box(
"Step 5: Quick-Thinking Setup",
"Provider and model for analysts & risk debaters (fast, high volume)"
"Provider and model for analysts & risk debaters (fast, high volume)",
)
)
quick_provider, quick_backend_url = select_llm_provider()
selected_shallow_thinker = select_shallow_thinking_agent(quick_provider)
quick_thinking_level, quick_reasoning_effort = _ask_provider_thinking_config(quick_provider)
quick_thinking_level, quick_reasoning_effort = _ask_provider_thinking_config(
quick_provider
)
# Step 6: Mid-thinking provider + model
console.print(
create_question_box(
"Step 6: Mid-Thinking Setup",
"Provider and model for researchers & trader (reasoning, argument formation)"
"Provider and model for researchers & trader (reasoning, argument formation)",
)
)
mid_provider, mid_backend_url = select_llm_provider()
selected_mid_thinker = select_mid_thinking_agent(mid_provider)
mid_thinking_level, mid_reasoning_effort = _ask_provider_thinking_config(mid_provider)
mid_thinking_level, mid_reasoning_effort = _ask_provider_thinking_config(
mid_provider
)
# Step 7: Deep-thinking provider + model
console.print(
create_question_box(
"Step 7: Deep-Thinking Setup",
"Provider and model for investment judge & risk manager (final decisions)"
"Provider and model for investment judge & risk manager (final decisions)",
)
)
deep_provider, deep_backend_url = select_llm_provider()
selected_deep_thinker = select_deep_thinking_agent(deep_provider)
deep_thinking_level, deep_reasoning_effort = _ask_provider_thinking_config(deep_provider)
deep_thinking_level, deep_reasoning_effort = _ask_provider_thinking_config(
deep_provider
)
return {
"ticker": selected_ticker,
@ -636,8 +647,12 @@ def save_report_to_disk(final_state, ticker: str, save_path: Path):
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"]))
(analysts_dir / "fundamentals.md").write_text(
final_state["fundamentals_report"]
)
analyst_parts.append(
("Fundamentals Analyst", final_state["fundamentals_report"])
)
if analyst_parts:
content = "\n\n".join(f"### {name}\n{text}" for name, text in analyst_parts)
sections.append(f"## I. Analyst Team Reports\n\n{content}")
@ -660,7 +675,9 @@ def save_report_to_disk(final_state, ticker: str, save_path: Path):
(research_dir / "manager.md").write_text(debate["judge_decision"])
research_parts.append(("Research Manager", debate["judge_decision"]))
if research_parts:
content = "\n\n".join(f"### {name}\n{text}" for name, text in research_parts)
content = "\n\n".join(
f"### {name}\n{text}" for name, text in research_parts
)
sections.append(f"## II. Research Team Decision\n\n{content}")
# 3. Trading
@ -668,7 +685,9 @@ def save_report_to_disk(final_state, ticker: str, save_path: Path):
trading_dir = save_path / "3_trading"
trading_dir.mkdir(exist_ok=True)
(trading_dir / "trader.md").write_text(final_state["trader_investment_plan"])
sections.append(f"## III. Trading Team Plan\n\n### Trader\n{final_state['trader_investment_plan']}")
sections.append(
f"## III. Trading Team Plan\n\n### Trader\n{final_state['trader_investment_plan']}"
)
# 4. Risk Management
if final_state.get("risk_debate_state"):
@ -696,7 +715,9 @@ def save_report_to_disk(final_state, ticker: str, save_path: Path):
portfolio_dir = save_path / "5_portfolio"
portfolio_dir.mkdir(exist_ok=True)
(portfolio_dir / "decision.md").write_text(risk["judge_decision"])
sections.append(f"## V. Portfolio Manager Decision\n\n### Portfolio Manager\n{risk['judge_decision']}")
sections.append(
f"## V. Portfolio Manager Decision\n\n### Portfolio Manager\n{risk['judge_decision']}"
)
# Write consolidated report
header = f"# Trading Analysis Report: {ticker}\n\nGenerated: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
@ -720,9 +741,15 @@ def display_complete_report(final_state):
if final_state.get("fundamentals_report"):
analysts.append(("Fundamentals Analyst", final_state["fundamentals_report"]))
if analysts:
console.print(Panel("[bold]I. Analyst Team Reports[/bold]", border_style="cyan"))
console.print(
Panel("[bold]I. Analyst Team Reports[/bold]", border_style="cyan")
)
for title, content in analysts:
console.print(Panel(Markdown(content), title=title, border_style="blue", padding=(1, 2)))
console.print(
Panel(
Markdown(content), title=title, border_style="blue", padding=(1, 2)
)
)
# II. Research Team Reports
if final_state.get("investment_debate_state"):
@ -735,14 +762,32 @@ def display_complete_report(final_state):
if debate.get("judge_decision"):
research.append(("Research Manager", debate["judge_decision"]))
if research:
console.print(Panel("[bold]II. Research Team Decision[/bold]", border_style="magenta"))
console.print(
Panel("[bold]II. Research Team Decision[/bold]", border_style="magenta")
)
for title, content in research:
console.print(Panel(Markdown(content), title=title, border_style="blue", padding=(1, 2)))
console.print(
Panel(
Markdown(content),
title=title,
border_style="blue",
padding=(1, 2),
)
)
# III. Trading Team
if final_state.get("trader_investment_plan"):
console.print(Panel("[bold]III. Trading Team Plan[/bold]", border_style="yellow"))
console.print(Panel(Markdown(final_state["trader_investment_plan"]), title="Trader", border_style="blue", padding=(1, 2)))
console.print(
Panel("[bold]III. Trading Team Plan[/bold]", border_style="yellow")
)
console.print(
Panel(
Markdown(final_state["trader_investment_plan"]),
title="Trader",
border_style="blue",
padding=(1, 2),
)
)
# IV. Risk Management Team
if final_state.get("risk_debate_state"):
@ -755,14 +800,36 @@ def display_complete_report(final_state):
if risk.get("neutral_history"):
risk_reports.append(("Neutral Analyst", risk["neutral_history"]))
if risk_reports:
console.print(Panel("[bold]IV. Risk Management Team Decision[/bold]", border_style="red"))
console.print(
Panel(
"[bold]IV. Risk Management Team Decision[/bold]", border_style="red"
)
)
for title, content in risk_reports:
console.print(Panel(Markdown(content), title=title, border_style="blue", padding=(1, 2)))
console.print(
Panel(
Markdown(content),
title=title,
border_style="blue",
padding=(1, 2),
)
)
# V. Portfolio Manager Decision
if risk.get("judge_decision"):
console.print(Panel("[bold]V. Portfolio Manager Decision[/bold]", border_style="green"))
console.print(Panel(Markdown(risk["judge_decision"]), title="Portfolio Manager", border_style="blue", padding=(1, 2)))
console.print(
Panel(
"[bold]V. Portfolio Manager Decision[/bold]", border_style="green"
)
)
console.print(
Panel(
Markdown(risk["judge_decision"]),
title="Portfolio Manager",
border_style="blue",
padding=(1, 2),
)
)
def update_research_team_status(status):
@ -822,6 +889,7 @@ def update_analyst_statuses(message_buffer, chunk):
if message_buffer.agent_status.get("Bull Researcher") == "pending":
message_buffer.update_agent_status("Bull Researcher", "in_progress")
def extract_content_string(content):
"""Extract string content from various message formats.
Returns None if no meaningful text content is found.
@ -830,7 +898,7 @@ def extract_content_string(content):
def is_empty(val):
"""Check if value is empty using Python's truthiness."""
if val is None or val == '':
if val is None or val == "":
return True
if isinstance(val, str):
s = val.strip()
@ -849,16 +917,17 @@ def extract_content_string(content):
return content.strip()
if isinstance(content, dict):
text = content.get('text', '')
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 '')
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))
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
@ -873,7 +942,7 @@ def classify_message_type(message) -> tuple[str, str | None]:
"""
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
content = extract_content_string(getattr(message, 'content', None))
content = extract_content_string(getattr(message, "content", None))
if isinstance(message, HumanMessage):
if content and content.strip() == "Continue":
@ -918,13 +987,15 @@ def parse_tool_call(tool_call) -> tuple[str, dict | str]:
args = getattr(tool_call, "args", getattr(tool_call, "arguments", {}))
return tool_name, args
def format_tool_args(args, max_length=80) -> str:
"""Format tool arguments for terminal display."""
result = str(args)
if len(result) > max_length:
return result[:max_length - 3] + "..."
return result[: max_length - 3] + "..."
return result
def run_analysis():
# First get all user selections
selections = get_user_selections()
@ -938,7 +1009,9 @@ def run_analysis():
config["quick_think_llm_provider"] = selections["quick_provider"]
config["quick_think_backend_url"] = selections["quick_backend_url"]
config["quick_think_google_thinking_level"] = selections.get("quick_thinking_level")
config["quick_think_openai_reasoning_effort"] = selections.get("quick_reasoning_effort")
config["quick_think_openai_reasoning_effort"] = selections.get(
"quick_reasoning_effort"
)
config["mid_think_llm"] = selections["mid_thinker"]
config["mid_think_llm_provider"] = selections["mid_provider"]
config["mid_think_backend_url"] = selections["mid_backend_url"]
@ -948,7 +1021,9 @@ def run_analysis():
config["deep_think_llm_provider"] = selections["deep_provider"]
config["deep_think_backend_url"] = selections["deep_backend_url"]
config["deep_think_google_thinking_level"] = selections.get("deep_thinking_level")
config["deep_think_openai_reasoning_effort"] = selections.get("deep_reasoning_effort")
config["deep_think_openai_reasoning_effort"] = selections.get(
"deep_reasoning_effort"
)
# Keep shared llm_provider/backend_url as a fallback (use quick as default)
config["llm_provider"] = selections["quick_provider"]
config["backend_url"] = selections["quick_backend_url"]
@ -986,6 +1061,7 @@ def run_analysis():
def save_message_decorator(obj, func_name):
func = getattr(obj, func_name)
@wraps(func)
def wrapper(*args, **kwargs):
func(*args, **kwargs)
@ -993,10 +1069,12 @@ def run_analysis():
content = content.replace("\n", " ") # Replace newlines with spaces
with open(log_file, "a", encoding="utf-8") as f:
f.write(f"{timestamp} [{message_type}] {content}\n")
return wrapper
def save_tool_call_decorator(obj, func_name):
func = getattr(obj, func_name)
@wraps(func)
def wrapper(*args, **kwargs):
func(*args, **kwargs)
@ -1004,24 +1082,34 @@ def run_analysis():
args_str = ", ".join(f"{k}={v}" for k, v in args.items())
with open(log_file, "a", encoding="utf-8") as f:
f.write(f"{timestamp} [Tool Call] {tool_name}({args_str})\n")
return wrapper
def save_report_section_decorator(obj, func_name):
func = getattr(obj, func_name)
@wraps(func)
def wrapper(section_name, content):
func(section_name, content)
if section_name in obj.report_sections and obj.report_sections[section_name] is not None:
if (
section_name in obj.report_sections
and obj.report_sections[section_name] is not None
):
content = obj.report_sections[section_name]
if content:
file_name = f"{section_name}.md"
with open(report_dir / file_name, "w", encoding="utf-8") as f:
f.write(content)
return wrapper
message_buffer.add_message = save_message_decorator(message_buffer, "add_message")
message_buffer.add_tool_call = save_tool_call_decorator(message_buffer, "add_tool_call")
message_buffer.update_report_section = save_report_section_decorator(message_buffer, "update_report_section")
message_buffer.add_tool_call = save_tool_call_decorator(
message_buffer, "add_tool_call"
)
message_buffer.update_report_section = save_report_section_decorator(
message_buffer, "update_report_section"
)
# Now start the display layout
layout = create_layout()
@ -1050,7 +1138,9 @@ def run_analysis():
spinner_text = (
f"Analyzing {selections['ticker']} on {selections['analysis_date']}..."
)
update_display(layout, spinner_text, stats_handler=stats_handler, start_time=start_time)
update_display(
layout, spinner_text, stats_handler=stats_handler, start_time=start_time
)
# Initialize state and get graph args with callbacks
init_agent_state = graph.propagator.create_initial_state(
@ -1117,7 +1207,9 @@ def run_analysis():
)
if message_buffer.agent_status.get("Trader") != "completed":
message_buffer.update_agent_status("Trader", "completed")
message_buffer.update_agent_status("Aggressive Analyst", "in_progress")
message_buffer.update_agent_status(
"Aggressive Analyst", "in_progress"
)
# Risk Management Team - Handle Risk Debate State
if chunk.get("risk_debate_state"):
@ -1128,33 +1220,65 @@ def run_analysis():
judge = risk_state.get("judge_decision", "").strip()
if agg_hist:
if message_buffer.agent_status.get("Aggressive Analyst") != "completed":
message_buffer.update_agent_status("Aggressive Analyst", "in_progress")
if (
message_buffer.agent_status.get("Aggressive Analyst")
!= "completed"
):
message_buffer.update_agent_status(
"Aggressive Analyst", "in_progress"
)
message_buffer.update_report_section(
"final_trade_decision", f"### Aggressive Analyst Analysis\n{agg_hist}"
"final_trade_decision",
f"### Aggressive Analyst Analysis\n{agg_hist}",
)
if con_hist:
if message_buffer.agent_status.get("Conservative Analyst") != "completed":
message_buffer.update_agent_status("Conservative Analyst", "in_progress")
if (
message_buffer.agent_status.get("Conservative Analyst")
!= "completed"
):
message_buffer.update_agent_status(
"Conservative Analyst", "in_progress"
)
message_buffer.update_report_section(
"final_trade_decision", f"### Conservative Analyst Analysis\n{con_hist}"
"final_trade_decision",
f"### Conservative Analyst Analysis\n{con_hist}",
)
if neu_hist:
if message_buffer.agent_status.get("Neutral Analyst") != "completed":
message_buffer.update_agent_status("Neutral Analyst", "in_progress")
if (
message_buffer.agent_status.get("Neutral Analyst")
!= "completed"
):
message_buffer.update_agent_status(
"Neutral Analyst", "in_progress"
)
message_buffer.update_report_section(
"final_trade_decision", f"### Neutral Analyst Analysis\n{neu_hist}"
"final_trade_decision",
f"### Neutral Analyst Analysis\n{neu_hist}",
)
if judge:
if message_buffer.agent_status.get("Portfolio Manager") != "completed":
message_buffer.update_agent_status("Portfolio Manager", "in_progress")
message_buffer.update_report_section(
"final_trade_decision", f"### Portfolio Manager Decision\n{judge}"
if (
message_buffer.agent_status.get("Portfolio Manager")
!= "completed"
):
message_buffer.update_agent_status(
"Portfolio Manager", "in_progress"
)
message_buffer.update_report_section(
"final_trade_decision",
f"### Portfolio Manager Decision\n{judge}",
)
message_buffer.update_agent_status(
"Aggressive Analyst", "completed"
)
message_buffer.update_agent_status(
"Conservative Analyst", "completed"
)
message_buffer.update_agent_status(
"Neutral Analyst", "completed"
)
message_buffer.update_agent_status(
"Portfolio Manager", "completed"
)
message_buffer.update_agent_status("Aggressive Analyst", "completed")
message_buffer.update_agent_status("Conservative Analyst", "completed")
message_buffer.update_agent_status("Neutral Analyst", "completed")
message_buffer.update_agent_status("Portfolio Manager", "completed")
# Update the display
update_display(layout, stats_handler=stats_handler, start_time=start_time)
@ -1188,12 +1312,13 @@ def run_analysis():
if save_choice in ("Y", "YES", ""):
default_path = get_ticker_dir(selections["analysis_date"], selections["ticker"])
save_path_str = typer.prompt(
"Save path (press Enter for default)",
default=str(default_path)
"Save path (press Enter for default)", default=str(default_path)
).strip()
save_path = Path(save_path_str)
try:
report_file = save_report_to_disk(final_state, selections["ticker"], save_path)
report_file = save_report_to_disk(
final_state, selections["ticker"], save_path
)
console.print(f"\n[green]✓ Report saved to:[/green] {save_path.resolve()}")
console.print(f" [dim]Complete report:[/dim] {report_file.name}")
except Exception as e:
@ -1221,14 +1346,18 @@ def run_analysis():
set_run_logger(None)
# Prompt to display full report
display_choice = typer.prompt("\nDisplay full report on screen?", default="Y").strip().upper()
display_choice = (
typer.prompt("\nDisplay full report on screen?", default="Y").strip().upper()
)
if display_choice in ("Y", "YES", ""):
display_complete_report(final_state)
def run_scan(date: Optional[str] = None):
"""Run the 3-phase LLM scanner pipeline via ScannerGraph."""
console.print(Panel("[bold green]Global Macro Scanner[/bold green]", border_style="green"))
console.print(
Panel("[bold green]Global Macro Scanner[/bold green]", border_style="green")
)
if date:
scan_date = date
else:
@ -1240,7 +1369,9 @@ def run_scan(date: Optional[str] = None):
save_dir.mkdir(parents=True, exist_ok=True)
console.print(f"[cyan]Running 3-phase macro scanner for {scan_date}...[/cyan]")
console.print("[dim]Phase 1: Geopolitical + Market Movers + Sector scans (parallel)[/dim]")
console.print(
"[dim]Phase 1: Geopolitical + Market Movers + Sector scans (parallel)[/dim]"
)
console.print("[dim]Phase 2: Industry Deep Dive[/dim]")
console.print("[dim]Phase 3: Macro Synthesis → stocks to investigate[/dim]\n")
@ -1248,7 +1379,9 @@ def run_scan(date: Optional[str] = None):
set_run_logger(run_logger)
try:
scanner = ScannerGraph(config=DEFAULT_CONFIG.copy(), callbacks=[run_logger.callback])
scanner = ScannerGraph(
config=DEFAULT_CONFIG.copy(), callbacks=[run_logger.callback]
)
with Live(Spinner("dots", text="Scanning..."), console=console, transient=True):
result = scanner.scan(scan_date)
except Exception as e:
@ -1256,8 +1389,13 @@ def run_scan(date: Optional[str] = None):
raise typer.Exit(1)
# Save reports
for key in ["geopolitical_report", "market_movers_report", "sector_performance_report",
"industry_deep_dive_report", "macro_scan_summary"]:
for key in [
"geopolitical_report",
"market_movers_report",
"sector_performance_report",
"industry_deep_dive_report",
"macro_scan_summary",
]:
content = result.get(key, "")
if content:
(save_dir / f"{key}.md").write_text(content)
@ -1291,7 +1429,6 @@ def run_scan(date: Optional[str] = None):
except (json.JSONDecodeError, KeyError, ValueError):
pass # Summary wasn't valid JSON — already printed as markdown
# Write observability log
run_logger.write_log(save_dir / "run_log.jsonl")
scan_summary = run_logger.summary()
@ -1310,14 +1447,18 @@ def run_scan(date: Optional[str] = None):
if result.get("market_movers_report"):
scan_parts.append(f"### Market Movers\n{result['market_movers_report']}")
if result.get("sector_performance_report"):
scan_parts.append(f"### Sector Performance\n{result['sector_performance_report']}")
scan_parts.append(
f"### Sector Performance\n{result['sector_performance_report']}"
)
if result.get("industry_deep_dive_report"):
scan_parts.append(f"### Industry Deep Dive\n{result['industry_deep_dive_report']}")
scan_parts.append(
f"### Industry Deep Dive\n{result['industry_deep_dive_report']}"
)
if result.get("macro_scan_summary"):
scan_parts.append(f"### Macro Scan Summary\n{result['macro_scan_summary']}")
macro_content = "\n\n".join(scan_parts)
if macro_content:
digest_path = append_to_digest(scan_date, "scan", "Market Scan", macro_content)
sync_to_notebooklm(digest_path, scan_date)
@ -1341,34 +1482,47 @@ def run_pipeline(
save_results,
)
console.print(Panel("[bold green]Macro → TradingAgents Pipeline[/bold green]", border_style="green"))
console.print(
Panel(
"[bold green]Macro → TradingAgents Pipeline[/bold green]",
border_style="green",
)
)
if macro_path_str is None:
macro_output = typer.prompt("Path to macro scan JSON")
else:
macro_output = macro_path_str
macro_path = Path(macro_output)
if not macro_path.exists():
console.print(f"[red]File not found: {macro_path}[/red]")
raise typer.Exit(1)
if min_conviction_opt is None:
min_conviction = typer.prompt("Minimum conviction (high/medium/low)", default="medium")
min_conviction = typer.prompt(
"Minimum conviction (high/medium/low)", default="medium"
)
else:
min_conviction = min_conviction_opt
if ticker_filter_list is None:
tickers_input = typer.prompt("Specific tickers (comma-separated, or blank for all)", default="")
ticker_filter = [t.strip() for t in tickers_input.split(",") if t.strip()] or None
tickers_input = typer.prompt(
"Specific tickers (comma-separated, or blank for all)", default=""
)
ticker_filter = [
t.strip() for t in tickers_input.split(",") if t.strip()
] or None
else:
ticker_filter = ticker_filter_list
if analysis_date_opt is None:
analysis_date = typer.prompt("Analysis date", default=datetime.datetime.now().strftime("%Y-%m-%d"))
analysis_date = typer.prompt(
"Analysis date", default=datetime.datetime.now().strftime("%Y-%m-%d")
)
else:
analysis_date = analysis_date_opt
if dry_run_opt is None:
dry_run = typer.confirm("Dry run (no API calls)?", default=False)
else:
@ -1378,7 +1532,9 @@ def run_pipeline(
macro_context, all_candidates = parse_macro_output(macro_path)
candidates = filter_candidates(all_candidates, min_conviction, ticker_filter)
console.print(f"\n[cyan]Candidates: {len(candidates)} of {len(all_candidates)} stocks passed filter[/cyan]")
console.print(
f"\n[cyan]Candidates: {len(candidates)} of {len(all_candidates)} stocks passed filter[/cyan]"
)
table = Table(title="Selected Stocks", box=box.ROUNDED)
table.add_column("Ticker", style="cyan bold")
@ -1403,9 +1559,13 @@ def run_pipeline(
run_logger = RunLogger()
set_run_logger(run_logger)
console.print(f"\n[cyan]Running TradingAgents for {len(candidates)} tickers...[/cyan]")
console.print(
f"\n[cyan]Running TradingAgents for {len(candidates)} tickers...[/cyan]"
)
try:
with Live(Spinner("dots", text="Analyzing..."), console=console, transient=True):
with Live(
Spinner("dots", text="Analyzing..."), console=console, transient=True
):
results = asyncio.run(
run_all_tickers(candidates, macro_context, config, analysis_date)
)
@ -1429,13 +1589,18 @@ def run_pipeline(
# Append to daily digest and sync to NotebookLM
from tradingagents.pipeline.macro_bridge import render_combined_summary
pipeline_summary = render_combined_summary(results, macro_context)
digest_path = append_to_digest(analysis_date, "pipeline", "Pipeline Summary", pipeline_summary)
digest_path = append_to_digest(
analysis_date, "pipeline", "Pipeline Summary", pipeline_summary
)
sync_to_notebooklm(digest_path, analysis_date)
successes = [r for r in results if not r.error]
failures = [r for r in results if r.error]
console.print(f"\n[green]Done: {len(successes)} succeeded, {len(failures)} failed[/green]")
console.print(
f"\n[green]Done: {len(successes)} succeeded, {len(failures)} failed[/green]"
)
console.print(f"Reports saved to: {output_dir.resolve()}")
if failures:
for r in failures:
@ -1450,7 +1615,9 @@ def analyze():
@app.command()
def scan(
date: Optional[str] = typer.Option(None, "--date", "-d", help="Scan date in YYYY-MM-DD format (default: today)"),
date: Optional[str] = typer.Option(
None, "--date", "-d", help="Scan date in YYYY-MM-DD format (default: today)"
),
):
"""Run 3-phase macro scanner (geopolitical → sector → synthesis)."""
run_scan(date=date)
@ -1469,7 +1636,11 @@ def run_portfolio(portfolio_id: str, date: str, macro_path: Path):
from tradingagents.graph.portfolio_graph import PortfolioGraph
from tradingagents.portfolio.repository import PortfolioRepository
console.print(Panel("[bold green]Portfolio Manager Execution[/bold green]", border_style="green"))
console.print(
Panel(
"[bold green]Portfolio Manager Execution[/bold green]", border_style="green"
)
)
if not macro_path.exists():
console.print(f"[red]Scan summary not found: {macro_path}[/red]")
@ -1483,38 +1654,46 @@ def run_portfolio(portfolio_id: str, date: str, macro_path: Path):
raise typer.Exit(1)
repo = PortfolioRepository()
# Check if portfolio exists
portfolio = repo.get_portfolio(portfolio_id)
if not portfolio:
console.print(f"[yellow]Portfolio '{portfolio_id}' not found. Please ensure it is created in the database.[/yellow]")
console.print(
f"[yellow]Portfolio '{portfolio_id}' not found. Please ensure it is created in the database.[/yellow]"
)
raise typer.Exit(1)
holdings = repo.get_holdings(portfolio_id)
candidates = scan_summary.get("stocks_to_investigate", [])
holding_tickers = [h.ticker for h in holdings]
all_tickers = set(candidates + holding_tickers)
console.print(f"[cyan]Fetching prices for {len(all_tickers)} tickers...[/cyan]")
prices = {}
for ticker in all_tickers:
try:
prices[ticker] = float(yf.Ticker(ticker).fast_info["lastPrice"])
except Exception as e:
console.print(f"[yellow]Warning: Could not fetch price for {ticker}: {e}[/yellow]")
console.print(
f"[yellow]Warning: Could not fetch price for {ticker}: {e}[/yellow]"
)
prices[ticker] = 0.0
console.print(f"[cyan]Running PortfolioGraph for '{portfolio_id}'...[/cyan]")
try:
with Live(Spinner("dots", text="Managing portfolio..."), console=console, transient=True):
with Live(
Spinner("dots", text="Managing portfolio..."),
console=console,
transient=True,
):
graph = PortfolioGraph(debug=False, repo=repo)
result = graph.run(
portfolio_id=portfolio_id,
date=date,
prices=prices,
scan_summary=scan_summary
scan_summary=scan_summary,
)
except Exception as e:
console.print(f"[red]Portfolio execution failed: {e}[/red]")
@ -1522,17 +1701,27 @@ def run_portfolio(portfolio_id: str, date: str, macro_path: Path):
console.print("[green]Portfolio execution completed successfully![/green]")
if "pm_decision" in result:
console.print(Panel(Markdown(str(result["pm_decision"])), title="PM Decision", border_style="blue"))
console.print(
Panel(
Markdown(str(result["pm_decision"])),
title="PM Decision",
border_style="blue",
)
)
@app.command()
def portfolio():
"""Run the Portfolio Manager Phase 6 workflow."""
console.print(Panel("[bold green]Portfolio Manager CLI[/bold green]", border_style="green"))
console.print(
Panel("[bold green]Portfolio Manager CLI[/bold green]", border_style="green")
)
portfolio_id = typer.prompt("Portfolio ID", default="main_portfolio")
date = typer.prompt("Analysis date", default=datetime.datetime.now().strftime("%Y-%m-%d"))
date = typer.prompt(
"Analysis date", default=datetime.datetime.now().strftime("%Y-%m-%d")
)
macro_output = typer.prompt("Path to macro scan JSON")
macro_path = Path(macro_output)
@ -1541,23 +1730,32 @@ def portfolio():
@app.command(name="check-portfolio")
def check_portfolio(
portfolio_id: str = typer.Option("main_portfolio", "--portfolio-id", "-p", help="Portfolio ID"),
date: Optional[str] = typer.Option(None, "--date", "-d", help="Analysis date in YYYY-MM-DD format (default: today)"),
portfolio_id: str = typer.Option(
"main_portfolio", "--portfolio-id", "-p", help="Portfolio ID"
),
date: Optional[str] = typer.Option(
None, "--date", "-d", help="Analysis date in YYYY-MM-DD format (default: today)"
),
):
"""Run Portfolio Manager to review current holdings only (no new candidates)."""
import json
import tempfile
console.print(Panel("[bold green]Portfolio Manager: Holdings Review[/bold green]", border_style="green"))
console.print(
Panel(
"[bold green]Portfolio Manager: Holdings Review[/bold green]",
border_style="green",
)
)
if date is None:
date = datetime.datetime.now().strftime("%Y-%m-%d")
# Create a dummy scan_summary with no candidates
dummy_scan = {"stocks_to_investigate": []}
with tempfile.NamedTemporaryFile("w+", delete=False, suffix=".json") as f:
json.dump(dummy_scan, f)
dummy_path = Path(f.name)
try:
run_portfolio(portfolio_id, date, dummy_path)
finally:
@ -1566,17 +1764,23 @@ def check_portfolio(
@app.command()
def auto(
portfolio_id: str = typer.Option("main_portfolio", "--portfolio-id", "-p", help="Portfolio ID"),
date: Optional[str] = typer.Option(None, "--date", "-d", help="Analysis date in YYYY-MM-DD format (default: today)"),
portfolio_id: str = typer.Option(
"main_portfolio", "--portfolio-id", "-p", help="Portfolio ID"
),
date: Optional[str] = typer.Option(
None, "--date", "-d", help="Analysis date in YYYY-MM-DD format (default: today)"
),
):
"""Run end-to-end: scan -> pipeline -> portfolio manager."""
console.print(Panel("[bold green]TradingAgents Auto Mode[/bold green]", border_style="green"))
console.print(
Panel("[bold green]TradingAgents Auto Mode[/bold green]", border_style="green")
)
if date is None:
date = datetime.datetime.now().strftime("%Y-%m-%d")
console.print("\n[bold magenta]--- Step 1: Market Scan ---[/bold magenta]")
run_scan(date=date)
console.print("\n[bold magenta]--- Step 2: Per-Ticker Pipeline ---[/bold magenta]")
macro_path = get_daily_dir(date) / "summary" / "scan_summary.json"
run_pipeline(
@ -1584,9 +1788,9 @@ def auto(
min_conviction_opt="medium",
ticker_filter_list=None,
analysis_date_opt=date,
dry_run_opt=False
dry_run_opt=False,
)
console.print("\n[bold magenta]--- Step 3: Portfolio Manager ---[/bold magenta]")
run_portfolio(portfolio_id, date, macro_path)