From 2fa430442ac905c5f25d16e50badce0cad289596 Mon Sep 17 00:00:00 2001 From: James Baker Kroner <9373098+jbkroner@users.noreply.github.com> Date: Sat, 5 Jul 2025 22:59:12 -0400 Subject: [PATCH] headless operation --- .python-version | 2 +- HEADLESS_USAGE.md | 128 ++++++++++++++++++++++ cli/main.py | 252 +++++++++++++++++++++++++++++++++++++------- config_example.json | 10 ++ requirements.txt | 1 + 5 files changed, 351 insertions(+), 42 deletions(-) create mode 100644 HEADLESS_USAGE.md create mode 100644 config_example.json diff --git a/.python-version b/.python-version index c8cfe395..2c073331 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.10 +3.11 diff --git a/HEADLESS_USAGE.md b/HEADLESS_USAGE.md new file mode 100644 index 00000000..d0e4e52d --- /dev/null +++ b/HEADLESS_USAGE.md @@ -0,0 +1,128 @@ +# Headless Mode Usage Guide + +TradingAgents now supports headless operation for automated trading analysis without interactive prompts. + +## Quick Start + +```bash +# Basic headless analysis +python -m cli.main --ticker TSLA --headless + +# Full configuration example +python -m cli.main \ + --ticker TSLA \ + --date 2024-05-10 \ + --analysts market,social,news,fundamentals \ + --depth deep \ + --llm-provider openai \ + --shallow-model gpt-4o-mini \ + --deep-model o1-mini \ + --output-dir ./reports \ + --headless \ + --quiet +``` + +## Command-Line Options + +| Option | Short | Description | Default | +|--------|-------|-------------|---------| +| `--ticker` | `-t` | Stock ticker symbol (required in headless mode) | None | +| `--date` | `-d` | Analysis date (YYYY-MM-DD) | Today's date | +| `--analysts` | `-a` | Comma-separated analysts list | `market,social,news,fundamentals` | +| `--depth` | | Research depth (`shallow`, `medium`, `deep`) | `medium` | +| `--llm-provider` | | LLM provider (`openai`, `anthropic`, `google`) | `openai` | +| `--backend-url` | | LLM backend URL | `https://api.openai.com/v1` | +| `--shallow-model` | | Model for quick thinking | `gpt-4o-mini` | +| `--deep-model` | | Model for deep thinking | `o1-mini` | +| `--output-dir` | `-o` | Output directory for reports | `./results` | +| `--config` | `-c` | Configuration file path | None | +| `--headless` | | Enable headless mode | Interactive mode | +| `--quiet` | `-q` | Suppress verbose output | Verbose output | + +## Configuration File + +Create a JSON configuration file for complex setups: + +```json +{ + "llm_provider": "openai", + "deep_think_llm": "o1-mini", + "quick_think_llm": "gpt-4o-mini", + "backend_url": "https://api.openai.com/v1", + "max_debate_rounds": 3, + "max_risk_discuss_rounds": 3, + "online_tools": true, + "results_dir": "./results" +} +``` + +Use with: `python -m cli.main --config config.json --ticker AAPL --headless` + +## Output Structure + +Reports are saved in: `{output_dir}/{ticker}/{date}/` + +- `reports/` - Individual analyst reports as markdown files +- `complete_report.md` - Combined analysis report +- `decision.json` - Final trading decision in JSON format + +## Examples + +### Automated Pipeline +```bash +# Run daily analysis for multiple stocks +for ticker in AAPL TSLA NVDA; do + python -m cli.main --ticker $ticker --headless --quiet > results_$ticker.json +done +``` + +### CI/CD Integration +```bash +# Quick analysis for deployment +python -m cli.main \ + --ticker SPY \ + --analysts market,news \ + --depth shallow \ + --headless \ + --quiet +``` + +### Custom Analysis +```bash +# Deep research with custom models +python -m cli.main \ + --ticker MSFT \ + --depth deep \ + --llm-provider anthropic \ + --shallow-model claude-3-5-haiku-latest \ + --deep-model claude-sonnet-4-0 \ + --headless +``` + +## Error Handling + +- Missing `--ticker` in headless mode: Command will fail with clear error message +- Invalid analyst names: Command will list valid options +- Invalid date format: Must be YYYY-MM-DD format +- Missing API keys: Will fail during analysis (set environment variables) + +## Environment Variables + +Required for analysis: +```bash +export OPENAI_API_KEY=your_openai_key +export FINNHUB_API_KEY=your_finnhub_key +``` + +## Integration with Scripts + +The headless mode is designed for: +- Automated trading systems +- Batch processing workflows +- CI/CD pipelines +- Scheduled analysis jobs +- API integrations + +Return codes: +- 0: Success +- 1: Error (invalid arguments, missing keys, etc.) \ No newline at end of file diff --git a/cli/main.py b/cli/main.py index 64616ee1..684417f3 100644 --- a/cli/main.py +++ b/cli/main.py @@ -1,8 +1,9 @@ -from typing import Optional +from typing import Optional, List import datetime import typer from pathlib import Path from functools import wraps +import json from rich.console import Console from rich.panel import Panel from rich.spinner import Spinner @@ -11,19 +12,22 @@ from rich.columns import Columns from rich.markdown import Markdown from rich.layout import Layout from rich.text import Text -from rich.live import Live 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.utils import ( + get_ticker as prompt_ticker, + get_analysis_date as prompt_analysis_date, + select_analysts, + select_research_depth, + select_llm_provider, + select_shallow_thinking_agent, + select_deep_thinking_agent, +) console = Console() @@ -31,6 +35,7 @@ app = typer.Typer( name="TradingAgents", help="TradingAgents CLI: Multi-Agents LLM Financial Trading Framework", add_completion=True, # Enable shell completion + no_args_is_help=False, # Don't show help when no args provided ) @@ -338,10 +343,10 @@ def update_display(layout, spinner_text=None): if spinner_text: messages_table.add_row("", "Spinner", spinner_text) - # Add a footer to indicate if messages were truncated + # Add a row to indicate if messages were truncated if len(all_messages) > max_messages: - messages_table.footer = ( - f"[dim]Showing last {max_messages} of {len(all_messages)} messages[/dim]" + messages_table.add_row( + "", "Info", f"[dim]Showing last {max_messages} of {len(all_messages)} messages[/dim]" ) layout["messages"].update( @@ -431,7 +436,7 @@ def get_user_selections(): "Step 1: Ticker Symbol", "Enter the ticker symbol to analyze", "SPY" ) ) - selected_ticker = get_ticker() + selected_ticker = prompt_ticker() # Step 2: Analysis date default_date = datetime.datetime.now().strftime("%Y-%m-%d") @@ -442,7 +447,7 @@ def get_user_selections(): default_date, ) ) - analysis_date = get_analysis_date() + analysis_date = prompt_analysis_date() # Step 3: Select analysts console.print( @@ -492,28 +497,6 @@ def get_user_selections(): } -def get_ticker(): - """Get ticker symbol from user input.""" - return typer.prompt("", default="SPY") - - -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]" - ) def display_complete_report(final_state): @@ -799,7 +782,7 @@ def run_analysis(): # Now start the display layout layout = create_layout() - with Live(layout, refresh_per_second=4) as live: + with Live(layout, refresh_per_second=4): # Initial display update_display(layout) @@ -1073,9 +1056,9 @@ def run_analysis(): trace.append(chunk) - # Get final state and decision + # Get final state final_state = trace[-1] - decision = graph.process_signal(final_state["final_trade_decision"]) + graph.process_signal(final_state["final_trade_decision"]) # Update all agent statuses to completed for agent in message_buffer.agent_status: @@ -1096,10 +1079,197 @@ def run_analysis(): update_display(layout) -@app.command() -def analyze(): - run_analysis() +def run_headless_analysis( + ticker: str, + analysis_date: str, + analysts: List[str], + research_depth: int, + llm_provider: str, + backend_url: str, + shallow_thinker: str, + deep_thinker: str, + output_dir: str, + quiet: bool = False, + config_file: Optional[str] = None, +): + """Run analysis in headless mode without interactive prompts.""" + + # Load config from file if provided + if config_file: + with open(config_file, 'r') as f: + file_config = json.load(f) + config = DEFAULT_CONFIG.copy() + config.update(file_config) + else: + config = DEFAULT_CONFIG.copy() + + # Override config with command line arguments + config["max_debate_rounds"] = research_depth + config["max_risk_discuss_rounds"] = research_depth + config["quick_think_llm"] = shallow_thinker + config["deep_think_llm"] = deep_thinker + config["backend_url"] = backend_url + config["llm_provider"] = llm_provider.lower() + config["results_dir"] = output_dir + + # Initialize the graph + graph = TradingAgentsGraph(analysts, config=config, debug=not quiet) + + # Create result directory + results_dir = Path(output_dir) / ticker / analysis_date + results_dir.mkdir(parents=True, exist_ok=True) + report_dir = results_dir / "reports" + report_dir.mkdir(parents=True, exist_ok=True) + + if not quiet: + console.print(f"[green]Starting analysis for {ticker} on {analysis_date}[/green]") + console.print(f"[cyan]Analysts: {', '.join(analysts)}[/cyan]") + console.print(f"[cyan]Research depth: {research_depth} rounds[/cyan]") + console.print(f"[cyan]Output directory: {output_dir}[/cyan]") + + # Initialize state and run analysis + init_agent_state = graph.propagator.create_initial_state(ticker, analysis_date) + args = graph.propagator.get_graph_args() + + # Stream the analysis + final_state = None + if not quiet: + # Show progress with spinner + with console.status("[bold green]Running analysis...") as status: + trace = [] + for chunk in graph.graph.stream(init_agent_state, **args): + trace.append(chunk) + if chunk.get("current_agent"): + status.update(f"[bold green]Running: {chunk['current_agent']}...") + final_state = trace[-1] + else: + # Run silently + trace = list(graph.graph.stream(init_agent_state, **args)) + final_state = trace[-1] + + # Process the final decision + decision = graph.process_signal(final_state["final_trade_decision"]) + + # Save reports + report_sections = { + "market_report": "Market Analysis", + "sentiment_report": "Social Sentiment", + "news_report": "News Analysis", + "fundamentals_report": "Fundamentals Analysis", + "investment_plan": "Research Team Decision", + "trader_investment_plan": "Trading Team Plan", + "final_trade_decision": "Portfolio Management Decision" + } + + # Save individual reports + for section, title in report_sections.items(): + if section in final_state and final_state[section]: + report_file = report_dir / f"{section}.md" + with open(report_file, 'w') as f: + f.write(f"# {title}\n\n{final_state[section]}") + + # Save complete report + complete_report = [] + complete_report.append(f"# Trading Analysis Report: {ticker}") + complete_report.append(f"**Date:** {analysis_date}") + complete_report.append(f"**Analysts:** {', '.join(analysts)}") + complete_report.append(f"**Research Depth:** {research_depth} rounds") + complete_report.append("") + + for section, title in report_sections.items(): + if section in final_state and final_state[section]: + complete_report.append(f"## {title}") + complete_report.append(final_state[section]) + complete_report.append("") + + complete_report_file = results_dir / "complete_report.md" + with open(complete_report_file, 'w') as f: + f.write("\n".join(complete_report)) + + # Save decision as JSON + decision_file = results_dir / "decision.json" + with open(decision_file, 'w') as f: + json.dump(decision, f, indent=2) + + if not quiet: + console.print("[green]Analysis completed![/green]") + console.print(f"[cyan]Reports saved to: {results_dir}[/cyan]") + console.print(f"[cyan]Decision: {decision}[/cyan]") + else: + # In quiet mode, just output the decision + print(json.dumps(decision, indent=2)) + + return decision + + +def main( + ticker: str = typer.Option(None, "--ticker", "-t", help="Stock ticker symbol (e.g., AAPL, TSLA)"), + date: str = typer.Option(None, "--date", "-d", help="Analysis date (YYYY-MM-DD)"), + analysts: str = typer.Option(None, "--analysts", "-a", help="Comma-separated list of analysts (market,social,news,fundamentals)"), + depth: str = typer.Option(None, "--depth", help="Research depth (shallow=1, medium=3, deep=5)"), + llm_provider: str = typer.Option(None, "--llm-provider", help="LLM provider (openai, anthropic, google)"), + backend_url: str = typer.Option(None, "--backend-url", help="LLM backend URL"), + shallow_model: str = typer.Option(None, "--shallow-model", help="Model for shallow thinking"), + deep_model: str = typer.Option(None, "--deep-model", help="Model for deep thinking"), + output_dir: str = typer.Option("./results", "--output-dir", "-o", help="Output directory for reports"), + config_file: Optional[str] = typer.Option(None, "--config", "-c", help="Path to configuration file"), + headless: bool = typer.Option(False, "--headless", help="Run in headless mode without interactive prompts"), + quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress verbose output (only show final results)"), +): + """Run trading analysis with either interactive or headless mode.""" + + if headless: + # Headless mode - validate required arguments + if not ticker: + raise typer.BadParameter("--ticker is required in headless mode") + + # Set defaults for optional arguments + if not date: + date = datetime.datetime.now().strftime("%Y-%m-%d") + if not analysts: + analysts = "market,social,news,fundamentals" + if not depth: + depth = "medium" + if not llm_provider: + llm_provider = "openai" + if not backend_url: + backend_url = "https://api.openai.com/v1" + if not shallow_model: + shallow_model = "gpt-4o-mini" + if not deep_model: + deep_model = "o1-mini" + + # Parse depth to number + depth_map = {"shallow": 1, "medium": 3, "deep": 5} + research_depth = depth_map.get(depth, 3) + + # Parse analysts list + analyst_list = [a.strip() for a in analysts.split(",")] + + # Validate analysts + valid_analysts = ["market", "social", "news", "fundamentals"] + for analyst in analyst_list: + if analyst not in valid_analysts: + raise typer.BadParameter(f"Invalid analyst: {analyst}. Valid options: {', '.join(valid_analysts)}") + + # Run headless analysis + run_headless_analysis( + ticker=ticker, + analysis_date=date, + analysts=analyst_list, + research_depth=research_depth, + llm_provider=llm_provider, + backend_url=backend_url, + shallow_thinker=shallow_model, + deep_thinker=deep_model, + output_dir=output_dir, + quiet=quiet, + config_file=config_file, + ) + else: + # Interactive mode (original behavior) + run_analysis() if __name__ == "__main__": - app() + typer.run(main) diff --git a/config_example.json b/config_example.json new file mode 100644 index 00000000..ee522c61 --- /dev/null +++ b/config_example.json @@ -0,0 +1,10 @@ +{ + "llm_provider": "openai", + "deep_think_llm": "o1-mini", + "quick_think_llm": "gpt-4o-mini", + "backend_url": "https://api.openai.com/v1", + "max_debate_rounds": 3, + "max_risk_discuss_rounds": 3, + "online_tools": true, + "results_dir": "./results" +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a6154cd2..75e3c8bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,5 +22,6 @@ redis chainlit rich questionary +typer langchain_anthropic langchain-google-genai