mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-26 22:42:40 +03:00
- selections.py asks what to run; run.py builds the graph, streams it to the live view and saves the report - main.py keeps the Typer app and its two commands
320 lines
13 KiB
Python
320 lines
13 KiB
Python
"""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,
|
|
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():
|
|
"""Ask for the run's settings, offering the previous run's answers."""
|
|
selections = _prompt_selections(load_last_run())
|
|
save_last_run(selections)
|
|
return selections
|
|
|
|
|
|
def _prompt_selections(prefs):
|
|
"""Walk the selection steps. ``prefs`` prefills, the environment skips."""
|
|
# Display ASCII art welcome message
|
|
with open(Path(__file__).parent / "static" / "welcome.txt", encoding="utf-8") as f:
|
|
welcome_ascii = f.read()
|
|
|
|
# Create welcome box content
|
|
welcome_content = f"{welcome_ascii}\n"
|
|
welcome_content += "[bold green]TradingAgents: Multi-Agents LLM Financial Trading Framework - CLI[/bold green]\n\n"
|
|
welcome_content += "[bold]Workflow Steps:[/bold]\n"
|
|
welcome_content += "I. Analyst Team → II. Research Team → III. Trader → IV. Risk Management → V. Portfolio Management\n\n"
|
|
welcome_content += (
|
|
"[dim]Built by [Tauric Research](https://github.com/TauricResearch)[/dim]"
|
|
)
|
|
|
|
# Create and center the welcome box
|
|
welcome_box = Panel(
|
|
welcome_content,
|
|
border_style="green",
|
|
padding=(1, 2),
|
|
title="Welcome to TradingAgents",
|
|
subtitle="Multi-Agents LLM Financial Trading Framework",
|
|
)
|
|
console.print(Align.center(welcome_box))
|
|
console.print()
|
|
console.print() # Add vertical space before announcements
|
|
|
|
# Fetch and display announcements (silent on failure)
|
|
announcements = fetch_announcements()
|
|
display_announcements(console, announcements)
|
|
|
|
# Create a boxed questionnaire for each step
|
|
def create_question_box(title, prompt, default=None):
|
|
box_content = f"[bold]{title}[/bold]\n"
|
|
box_content += f"[dim]{prompt}[/dim]"
|
|
if default:
|
|
box_content += f"\n[dim]Default: {default}[/dim]"
|
|
return Panel(box_content, border_style="blue", padding=(1, 2))
|
|
|
|
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
|
|
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
|
|
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
|
|
console.print(
|
|
create_question_box(
|
|
"Step 4: Analysts Team", "Select your LLM analyst agents for the analysis"
|
|
)
|
|
)
|
|
prefs = sanitize(prefs, asset_type.value)
|
|
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:
|
|
# 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]"
|
|
)
|