"""The interactive choices for a run: ticker, date, analysts, depth, provider and models.""" import datetime import os from pathlib import Path import typer from rich.align import Align from rich.panel import Panel from cli.announcements import display_announcements, fetch_announcements from cli.display import ( console, ) from cli.prefs import load_last_run, sanitize, save_last_run from cli.prompts import ( ask_anthropic_effort, ask_gemini_thinking_config, ask_glm_region, ask_minimax_region, ask_openai_reasoning_effort, ask_output_language, ask_qwen_region, confirm_ollama_endpoint, detect_asset_type, ensure_api_key, get_ticker, parse_analysis_date, parse_analysts, parse_ticker, prompt_openai_compatible_url, resolve_backend_url, select_analysts, select_deep_thinking_agent, select_llm_provider, select_research_depth, select_shallow_thinking_agent, ) from tradingagents.default_config import DEFAULT_CONFIG def get_user_selections(flags=None): """Ask for the run's settings, offering the previous run's answers.""" selections = _prompt_selections(load_last_run(), flags or {}) save_last_run(selections) return selections def unattended_gaps(flags) -> list[str]: """The flags and environment variables a run with no terminal still needs.""" env = os.environ.get gaps = [f"--{name}" for name in ("ticker", "date", "analysts") if flags.get(name) is None] gaps += [f"--{name} or --no-{name}" for name in ("save", "show") if flags.get(name) is None] if not env("TRADINGAGENTS_OUTPUT_LANGUAGE"): gaps.append("TRADINGAGENTS_OUTPUT_LANGUAGE") if not (env("TRADINGAGENTS_MAX_DEBATE_ROUNDS") and env("TRADINGAGENTS_MAX_RISK_ROUNDS")): gaps.append("TRADINGAGENTS_MAX_DEBATE_ROUNDS and TRADINGAGENTS_MAX_RISK_ROUNDS") if not env("TRADINGAGENTS_LLM_PROVIDER"): gaps.append("TRADINGAGENTS_LLM_PROVIDER") if not (env("TRADINGAGENTS_QUICK_THINK_LLM") or env("TRADINGAGENTS_DEEP_THINK_LLM")): gaps.append("TRADINGAGENTS_QUICK_THINK_LLM or TRADINGAGENTS_DEEP_THINK_LLM") return gaps def _from_flag(parse, value, *args): """A flag's value through the same check its prompt applies; a bad one ends the run.""" try: return parse(value, *args) except ValueError as exc: console.print(f"[red]{exc}[/red]") raise typer.Exit(code=1) from None def _prompt_selections(prefs, flags): """Walk the selection steps. ``prefs`` prefills; flags and the environment skip.""" with open(Path(__file__).parent / "static" / "welcome.txt", encoding="utf-8") as f: welcome_ascii = f.read() welcome_content = f"{welcome_ascii}\n" welcome_content += "[bold green]TradingAgents: Multi-Agents LLM Financial Trading Framework - CLI[/bold green]\n\n" welcome_content += "[bold]Workflow Steps:[/bold]\n" welcome_content += "I. Analyst Team → II. Research Team → III. Trader → IV. Risk Management → V. Portfolio Management\n\n" welcome_content += ( "[dim]Built by [Tauric Research](https://github.com/TauricResearch)[/dim]" ) welcome_box = Panel( welcome_content, border_style="green", padding=(1, 2), title="Welcome to TradingAgents", subtitle="Multi-Agents LLM Financial Trading Framework", ) console.print(Align.center(welcome_box)) console.print() console.print() # Add vertical space before announcements # Fetch and display announcements (silent on failure) announcements = fetch_announcements() display_announcements(console, announcements) 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)) def thinking_value_or_prompt(env_var, config_key, label, box_title, box_body, prompt_fn): """Return the env-configured reasoning/thinking value, or prompt for it. When ``env_var`` is set the interactive choice is skipped and the value the env overlay placed on DEFAULT_CONFIG is used — mirroring the env-precedence rule applied to the other selection steps. """ if os.environ.get(env_var): value = DEFAULT_CONFIG[config_key] console.print(f"[green]✓ {label} from environment:[/green] {value}") return value console.print(create_question_box(box_title, box_body)) return prompt_fn() # Step 1: Ticker symbol if flags.get("ticker") is not None: selected_ticker = _from_flag(parse_ticker, flags["ticker"]) console.print(f"[green]✓ Ticker from --ticker:[/green] {selected_ticker}") else: console.print( create_question_box( "Step 1: Ticker Symbol", "Enter the ticker, with exchange suffix when needed (e.g. SPY, 0700.HK, BTC-USD)", "SPY", ) ) selected_ticker = get_ticker() asset_type = detect_asset_type(selected_ticker) # Only announce when it's not the default stock path, to avoid printing # "stock" on every run. if asset_type.value != "stock": console.print( f"[green]Detected asset type:[/green] {asset_type.value}" ) # Step 2: Analysis date if flags.get("date") is not None: analysis_date = _from_flag(parse_analysis_date, flags["date"]) console.print(f"[green]✓ Analysis date from --date:[/green] {analysis_date}") else: 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: Output language (skipped when set via TRADINGAGENTS_OUTPUT_LANGUAGE) if os.environ.get("TRADINGAGENTS_OUTPUT_LANGUAGE"): output_language = DEFAULT_CONFIG["output_language"] console.print( f"[green]✓ Output language from environment:[/green] {output_language}" ) else: console.print( create_question_box( "Step 3: Output Language", "Select the language for analyst reports and final decision" ) ) output_language = ask_output_language(prefs.get("output_language")) # Step 4: Select analysts prefs = sanitize(prefs, asset_type.value) if flags.get("analysts") is not None: selected_analysts = _from_flag(parse_analysts, flags["analysts"], asset_type) else: console.print( create_question_box( "Step 4: Analysts Team", "Select your LLM analyst agents for the analysis" ) ) selected_analysts = select_analysts(asset_type, prefs.get("analysts")) console.print( f"[green]Selected analysts:[/green] {', '.join(analyst.value for analyst in selected_analysts)}" ) # Step 5: Research depth (skipped when both round counts are set via env). # Research depth maps to the debate + risk round counts; when both are # supplied through TRADINGAGENTS_MAX_DEBATE_ROUNDS / _MAX_RISK_ROUNDS we keep # the run non-interactive and honor the env values (#977). depth_from_env = bool(os.environ.get("TRADINGAGENTS_MAX_DEBATE_ROUNDS")) and bool( os.environ.get("TRADINGAGENTS_MAX_RISK_ROUNDS") ) if depth_from_env: selected_research_depth = DEFAULT_CONFIG["max_debate_rounds"] console.print( f"[green]✓ Research depth from environment:[/green] " f"{DEFAULT_CONFIG['max_debate_rounds']} debate / " f"{DEFAULT_CONFIG['max_risk_discuss_rounds']} risk rounds" ) else: console.print( create_question_box( "Step 5: Research Depth", "Select your research depth level" ) ) selected_research_depth = select_research_depth(prefs.get("research_depth")) # Step 6: LLM Provider (skipped when set via TRADINGAGENTS_LLM_PROVIDER). # The backend URL comes from TRADINGAGENTS_LLM_BACKEND_URL when set, # otherwise the provider's default endpoint — the same value the menu # would have picked. provider_from_env = bool(os.environ.get("TRADINGAGENTS_LLM_PROVIDER")) if provider_from_env: selected_llm_provider = DEFAULT_CONFIG["llm_provider"].lower() backend_url = resolve_backend_url( selected_llm_provider, env_url=DEFAULT_CONFIG["backend_url"] ) console.print(f"[green]✓ LLM provider from environment:[/green] {selected_llm_provider}") console.print(f"[green]✓ Backend URL:[/green] {backend_url}") # Still confirm/persist the API key so the run doesn't fail later. ensure_api_key(selected_llm_provider) else: console.print( create_question_box( "Step 6: LLM Provider", "Select your LLM provider" ) ) selected_llm_provider, backend_url = select_llm_provider(prefs.get("llm_provider")) # Providers with regional endpoints prompt for the region as a secondary # step so the main dropdown stays clean (mainland China and international # accounts cannot share API keys). if selected_llm_provider == "qwen": selected_llm_provider, backend_url = ask_qwen_region() elif selected_llm_provider == "minimax": selected_llm_provider, backend_url = ask_minimax_region() elif selected_llm_provider == "glm": selected_llm_provider, backend_url = ask_glm_region() # Honor an explicit env backend URL even when the provider was chosen # interactively, so it isn't overwritten by the menu default (#978). backend_url = resolve_backend_url( selected_llm_provider, backend_url, env_url=DEFAULT_CONFIG["backend_url"] ) # The generic OpenAI-compatible endpoint has no default; ask for it if # neither the menu nor the environment supplied one. if selected_llm_provider == "openai_compatible" and not backend_url: remembered_url = (prefs.get("backend_url") if prefs.get("llm_provider") == selected_llm_provider else None) backend_url = prompt_openai_compatible_url(remembered_url) # For Ollama, surface the resolved endpoint (OLLAMA_BASE_URL vs default) # before model selection so it's obvious where we're connecting. if selected_llm_provider == "ollama": confirm_ollama_endpoint(backend_url) # Confirm the provider's API key is present; prompt the user to paste # one and persist it to .env if it's missing, so the analysis run # doesn't fail later at the first API call. ensure_api_key(selected_llm_provider) # Step 7: Thinking agents (skipped when either model is set via environment) if os.environ.get("TRADINGAGENTS_QUICK_THINK_LLM") or os.environ.get("TRADINGAGENTS_DEEP_THINK_LLM"): selected_shallow_thinker = DEFAULT_CONFIG["quick_think_llm"] selected_deep_thinker = DEFAULT_CONFIG["deep_think_llm"] console.print( f"[green]✓ Thinking agents from environment:[/green] " f"quick={selected_shallow_thinker}, deep={selected_deep_thinker}" ) else: console.print( create_question_box( "Step 7: Thinking Agents", "Select your thinking agents for analysis" ) ) remembered = prefs if prefs.get("llm_provider") == selected_llm_provider else {} selected_shallow_thinker = select_shallow_thinking_agent( selected_llm_provider, remembered.get("quick_think_llm") ) selected_deep_thinker = select_deep_thinking_agent( selected_llm_provider, remembered.get("deep_think_llm") ) # Step 8: Provider-specific reasoning/thinking configuration. Each knob is # settable via its TRADINGAGENTS_* env var; when that var is set (or the # provider itself came from env) the prompt is skipped and the configured # value is used — same env-precedence rule as the steps above. None = each # provider's own default. thinking_level = None reasoning_effort = None anthropic_effort = None provider_lower = selected_llm_provider.lower() if provider_from_env: thinking_level = DEFAULT_CONFIG["google_thinking_level"] reasoning_effort = DEFAULT_CONFIG["openai_reasoning_effort"] anthropic_effort = DEFAULT_CONFIG["anthropic_effort"] elif provider_lower == "google": thinking_level = thinking_value_or_prompt( "TRADINGAGENTS_GOOGLE_THINKING_LEVEL", "google_thinking_level", "Gemini thinking mode", "Step 8: Thinking Mode", "Configure Gemini thinking mode", ask_gemini_thinking_config, ) elif provider_lower == "openai": reasoning_effort = thinking_value_or_prompt( "TRADINGAGENTS_OPENAI_REASONING_EFFORT", "openai_reasoning_effort", "Reasoning effort", "Step 8: Reasoning Effort", "Configure OpenAI reasoning effort level", ask_openai_reasoning_effort, ) elif provider_lower == "anthropic": anthropic_effort = thinking_value_or_prompt( "TRADINGAGENTS_ANTHROPIC_EFFORT", "anthropic_effort", "Claude effort", "Step 8: Effort Level", "Configure Claude effort level", ask_anthropic_effort, ) return { "ticker": selected_ticker, "asset_type": asset_type.value, "analysis_date": analysis_date, "analysts": selected_analysts, "research_depth": selected_research_depth, "llm_provider": selected_llm_provider.lower(), "backend_url": backend_url, "quick_think_llm": selected_shallow_thinker, "deep_think_llm": selected_deep_thinker, "google_thinking_level": thinking_level, "openai_reasoning_effort": reasoning_effort, "anthropic_effort": anthropic_effort, "output_language": output_language, } 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: return parse_analysis_date(date_str) except ValueError as exc: console.print(f"[red]Error: {exc}[/red]")