mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-27 23:12:39 +03:00
- --ticker, --date and --analysts answer their steps, checked as the prompts check them; --save/--no-save and --show/--no-show answer the questions after the run - with no terminal, the run stops before it starts and names every flag or TRADINGAGENTS_* variable still needed; a missing API key and an announcement no longer wait for input - README shows an unattended run
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import getpass
|
||||
import sys
|
||||
|
||||
import requests
|
||||
from rich.console import Console
|
||||
@@ -46,7 +47,7 @@ def display_announcements(console: Console, data: dict) -> None:
|
||||
)
|
||||
console.print(panel)
|
||||
|
||||
if require_attention:
|
||||
if require_attention and sys.stdin.isatty():
|
||||
getpass.getpass("Press Enter to continue...")
|
||||
else:
|
||||
console.print()
|
||||
|
||||
+18
-2
@@ -47,8 +47,23 @@ def analyze(
|
||||
help="JSON file with current holdings and cash, so the trader, risk and "
|
||||
"portfolio agents size against your actual position.",
|
||||
),
|
||||
ticker: str = typer.Option(None, "--ticker", help="Ticker to analyze, e.g. NVDA or 0700.HK; skips the prompt"),
|
||||
date: str = typer.Option(None, "--date", help="Analysis date, YYYY-MM-DD; skips the prompt"),
|
||||
analysts: str = typer.Option(
|
||||
None, "--analysts", help="Comma-separated analysts, e.g. market,news; skips the prompt"
|
||||
),
|
||||
save: bool | None = typer.Option(
|
||||
None, "--save/--no-save", help="Save the report under results_dir without asking"
|
||||
),
|
||||
show: bool | None = typer.Option(
|
||||
None, "--show/--no-show", help="Show the full report at the end without asking"
|
||||
),
|
||||
):
|
||||
"""Run an analysis. This is what a bare `tradingagents` does."""
|
||||
"""Run an analysis. This is what a bare `tradingagents` does.
|
||||
|
||||
Flags answer their questions; with provider, models, depth and language also
|
||||
set through TRADINGAGENTS_* variables, the run asks nothing.
|
||||
"""
|
||||
if ctx.invoked_subcommand is not None:
|
||||
return
|
||||
if clear_checkpoints:
|
||||
@@ -64,7 +79,8 @@ def analyze(
|
||||
raise typer.Exit(code=1) from None
|
||||
|
||||
try:
|
||||
run_analysis(checkpoint=checkpoint, portfolio=portfolio_context)
|
||||
flags = {"ticker": ticker, "date": date, "analysts": analysts, "save": save, "show": show}
|
||||
run_analysis(checkpoint=checkpoint, portfolio=portfolio_context, flags=flags)
|
||||
except _NO_CONSOLE_ERRORS:
|
||||
# A terminal with no console buffer cannot host the interactive prompts.
|
||||
# Emit one actionable line on stderr instead of a prompt_toolkit
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import questionary
|
||||
import typer
|
||||
from dotenv import find_dotenv, set_key
|
||||
|
||||
from cli.display import console
|
||||
@@ -60,6 +63,41 @@ def get_ticker() -> str:
|
||||
return normalize_ticker_symbol(ticker) if ticker.strip() else "SPY"
|
||||
|
||||
|
||||
def parse_ticker(value: str) -> str:
|
||||
"""A ticker given on the command line, canonical; empty or malformed is refused."""
|
||||
if not value.strip() or not is_valid_ticker_input(value):
|
||||
raise ValueError(f"not a ticker symbol: {value!r} (e.g. {TICKER_INPUT_EXAMPLES})")
|
||||
return normalize_ticker_symbol(value)
|
||||
|
||||
|
||||
def parse_analysis_date(value: str) -> str:
|
||||
"""An analysis date as YYYY-MM-DD, today or earlier."""
|
||||
try:
|
||||
day = datetime.datetime.strptime(value.strip(), "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
raise ValueError(f"not a date: {value!r}; use YYYY-MM-DD") from None
|
||||
if day > datetime.date.today():
|
||||
raise ValueError(f"{value} is in the future")
|
||||
return day.isoformat()
|
||||
|
||||
|
||||
def parse_analysts(value: str, asset_type: AssetType) -> list[AnalystType]:
|
||||
"""Comma-separated analyst names, in the canonical order, checked against the asset."""
|
||||
# "sentiment" is the name users see; the analyst's key is "social".
|
||||
names = [{"sentiment": "social"}.get(n, n) for n in (n.strip().lower() for n in value.split(",")) if n]
|
||||
if not names:
|
||||
raise ValueError("name at least one analyst")
|
||||
known = {a.value: a for a in AnalystType}
|
||||
unknown = [n for n in names if n not in known]
|
||||
if unknown:
|
||||
raise ValueError(f"unknown analyst {', '.join(unknown)}; choose from {', '.join(known)}")
|
||||
available = filter_analysts_for_asset_type(list(known.values()), asset_type)
|
||||
unavailable = [n for n in names if known[n] not in available]
|
||||
if unavailable:
|
||||
raise ValueError(f"{', '.join(unavailable)} is not available for {asset_type.value}")
|
||||
return [a for a in available if a.value in names]
|
||||
|
||||
|
||||
def normalize_ticker_symbol(ticker: str) -> str:
|
||||
"""Resolve user input to its canonical Yahoo symbol (single source of truth).
|
||||
|
||||
@@ -611,6 +649,10 @@ def ensure_api_key(provider: str) -> str | None:
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
if not sys.stdin.isatty():
|
||||
console.print(f"[red]{env_var} is not set; there is no terminal to ask for it.[/red]")
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
console.print(
|
||||
f"\n[yellow]{env_var} is not set in your environment.[/yellow]"
|
||||
)
|
||||
|
||||
+35
-18
@@ -2,6 +2,7 @@
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
@@ -21,7 +22,7 @@ from cli.display import (
|
||||
update_display,
|
||||
update_research_team_status,
|
||||
)
|
||||
from cli.selections import get_user_selections
|
||||
from cli.selections import get_user_selections, unattended_gaps
|
||||
from cli.stats_handler import StatsCallbackHandler
|
||||
from tradingagents.agents.rating import is_review, run_rating
|
||||
from tradingagents.dataflows.symbols import safe_ticker_component
|
||||
@@ -93,9 +94,19 @@ def _build_run_config(selections: dict, checkpoint: bool | None) -> dict:
|
||||
return config
|
||||
|
||||
|
||||
def run_analysis(checkpoint: bool | None = None, portfolio=None):
|
||||
# First get all user selections
|
||||
selections = get_user_selections()
|
||||
def run_analysis(checkpoint: bool | None = None, portfolio=None, flags=None):
|
||||
flags = flags or {}
|
||||
# With no terminal nothing can answer a prompt: name every question still
|
||||
# open before any model is called, rather than stopping at the first one.
|
||||
if not (sys.stdin and sys.stdin.isatty()):
|
||||
gaps = unattended_gaps(flags)
|
||||
if gaps:
|
||||
console.print("[red]No terminal to answer the setup questions. Set:[/red]")
|
||||
for gap in gaps:
|
||||
console.print(f" {gap}")
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
selections = get_user_selections(flags)
|
||||
|
||||
config = _build_run_config(selections, checkpoint)
|
||||
|
||||
@@ -366,29 +377,35 @@ def run_analysis(checkpoint: bool | None = None, portfolio=None):
|
||||
)
|
||||
console.print(f"[dim]{analyst_wall_time_tracker.format_summary()}[/dim]")
|
||||
|
||||
# Prompt to save report
|
||||
save_choice = typer.prompt("Save report?", default="Y").strip().upper()
|
||||
if save_choice in ("Y", "YES", ""):
|
||||
_offer_reports(final_state, graph, config, selections["ticker"],
|
||||
save=flags.get("save"), show=flags.get("show"))
|
||||
|
||||
|
||||
def _offer_reports(final_state, graph, config, ticker, save=None, show=None):
|
||||
"""Save the report tree and show it; ``save``/``show`` answer the questions when given."""
|
||||
asked = save is None
|
||||
if asked:
|
||||
save = typer.prompt("Save report?", default="Y").strip().upper() in ("Y", "YES", "")
|
||||
if save:
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
# Under results_dir, not the working directory: in Docker the working
|
||||
# directory is inside the container and the report goes with it, while
|
||||
# results_dir is the mounted volume the rest of the run already writes to.
|
||||
default_path = (Path(config["results_dir"]) / "reports"
|
||||
/ f"{safe_ticker_component(selections['ticker'])}_{timestamp}")
|
||||
save_path_str = typer.prompt(
|
||||
"Save path (press Enter for default)",
|
||||
default=str(default_path)
|
||||
).strip()
|
||||
save_path = Path(save_path_str)
|
||||
save_path = (Path(config["results_dir"]) / "reports"
|
||||
/ f"{safe_ticker_component(ticker)}_{timestamp}")
|
||||
if asked: # someone at the prompt may pick another folder
|
||||
save_path = Path(typer.prompt(
|
||||
"Save path (press Enter for default)", default=str(save_path)
|
||||
).strip())
|
||||
try:
|
||||
report_file = write_report_tree(final_state, selections["ticker"], save_path,
|
||||
report_file = write_report_tree(final_state, ticker, save_path,
|
||||
settings=graph.run_settings())
|
||||
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:
|
||||
console.print(f"[red]Error saving report: {e}[/red]")
|
||||
|
||||
# Prompt to display full report
|
||||
display_choice = typer.prompt("\nDisplay full report on screen?", default="Y").strip().upper()
|
||||
if display_choice in ("Y", "YES", ""):
|
||||
if show is None:
|
||||
show = typer.prompt("\nDisplay full report on screen?", default="Y").strip().upper() in ("Y", "YES", "")
|
||||
if show:
|
||||
display_complete_report(final_state)
|
||||
|
||||
+67
-35
@@ -25,6 +25,9 @@ from cli.prompts import (
|
||||
detect_asset_type,
|
||||
ensure_api_key,
|
||||
get_ticker,
|
||||
parse_analysis_date,
|
||||
parse_analysts,
|
||||
parse_ticker,
|
||||
prompt_openai_compatible_url,
|
||||
resolve_backend_url,
|
||||
select_analysts,
|
||||
@@ -36,15 +39,40 @@ from cli.prompts import (
|
||||
from tradingagents.default_config import DEFAULT_CONFIG
|
||||
|
||||
|
||||
def get_user_selections():
|
||||
def get_user_selections(flags=None):
|
||||
"""Ask for the run's settings, offering the previous run's answers."""
|
||||
selections = _prompt_selections(load_last_run())
|
||||
selections = _prompt_selections(load_last_run(), flags or {})
|
||||
save_last_run(selections)
|
||||
return selections
|
||||
|
||||
|
||||
def _prompt_selections(prefs):
|
||||
"""Walk the selection steps. ``prefs`` prefills, the environment skips."""
|
||||
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()
|
||||
|
||||
@@ -93,14 +121,18 @@ def _prompt_selections(prefs):
|
||||
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",
|
||||
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()
|
||||
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.
|
||||
@@ -110,15 +142,19 @@ def _prompt_selections(prefs):
|
||||
)
|
||||
|
||||
# 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,
|
||||
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()
|
||||
analysis_date = get_analysis_date()
|
||||
|
||||
# Step 3: Output language (skipped when set via TRADINGAGENTS_OUTPUT_LANGUAGE)
|
||||
if os.environ.get("TRADINGAGENTS_OUTPUT_LANGUAGE"):
|
||||
@@ -136,13 +172,16 @@ def _prompt_selections(prefs):
|
||||
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"))
|
||||
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)}"
|
||||
)
|
||||
@@ -303,13 +342,6 @@ def get_analysis_date():
|
||||
"", 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]"
|
||||
)
|
||||
return parse_analysis_date(date_str)
|
||||
except ValueError as exc:
|
||||
console.print(f"[red]Error: {exc}[/red]")
|
||||
|
||||
Reference in New Issue
Block a user