headless operation
This commit is contained in:
parent
84a43853c4
commit
2fa430442a
|
|
@ -1 +1 @@
|
|||
3.10
|
||||
3.11
|
||||
|
|
|
|||
|
|
@ -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.)
|
||||
252
cli/main.py
252
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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -22,5 +22,6 @@ redis
|
|||
chainlit
|
||||
rich
|
||||
questionary
|
||||
typer
|
||||
langchain_anthropic
|
||||
langchain-google-genai
|
||||
|
|
|
|||
Loading…
Reference in New Issue