diff --git a/README.md b/README.md
index cac18691..11ace186 100644
--- a/README.md
+++ b/README.md
@@ -126,10 +126,16 @@ export OPENAI_API_KEY=$YOUR_OPENAI_API_KEY
### CLI Usage
-You can also try out the CLI directly by running:
+You can run the CLI analysis with the following command (recommended, default):
```bash
python -m cli.main
```
+
+Or, you can explicitly specify the analyze command (optional, for clarity):
+```bash
+python -m cli.main analyze
+```
+
You will see a screen where you can select your desired tickers, date, LLMs, research depth, etc.
@@ -146,6 +152,60 @@ An interface will appear showing results as they load, letting you track the age
+#### Optional Report Output
+
+To save the final consolidated report to files after analysis, use the `--save-reports` flag (works with both default and explicit usage):
+
+```bash
+# Save reports in both Markdown and JSON formats
+python -m cli.main --save-reports --format both
+
+# Save reports only in Markdown format
+python -m cli.main --save-reports --format markdown
+
+# Save reports only in JSON format
+python -m cli.main --save-reports --format json
+```
+
+Or, with the explicit command:
+```bash
+python -m cli.main analyze --save-reports --format both
+```
+
+Reports are saved to:
+```
+results///reports/
+├── final_report.md # Consolidated Markdown report
+├── final_report.json # Consolidated JSON report
+├── market_report.md # Individual analyst reports
+├── sentiment_report.md
+├── news_report.md
+├── fundamentals_report.md
+├── investment_plan.md
+├── trader_investment_plan.md
+└── final_trade_decision.md
+```
+
+#### Generate Reports from Existing Data
+
+If you want to generate the final consolidated report from existing analysis data without rerunning the analysis:
+
+```bash
+# Generate Markdown report (default)
+python -m cli.main save-report GOOG 2025-07-05
+
+# Generate both Markdown and JSON reports
+python -m cli.main save-report GOOG 2025-07-05 --format both
+
+# Generate only JSON report
+python -m cli.main save-report GOOG 2025-07-05 --format json
+```
+
+The final report includes:
+- **Analysis Summary**: Ticker, date, agent status, and completion statistics
+- **Agent Status Summary**: Status of all agents organized by teams
+- **Complete Analysis**: All analyst reports, research decisions, trading plans, and final portfolio decisions
+
## TradingAgents Package
### Implementation Details
@@ -163,7 +223,7 @@ from tradingagents.default_config import DEFAULT_CONFIG
ta = TradingAgentsGraph(debug=True, config=DEFAULT_CONFIG.copy())
# forward propagate
-_, decision = ta.propagate("NVDA", "2024-05-10")
+final_state, decision = ta.propagate("NVDA", "2024-05-10")
print(decision)
```
@@ -184,10 +244,54 @@ config["online_tools"] = True # Use online tools or cached data
ta = TradingAgentsGraph(debug=True, config=config)
# forward propagate
-_, decision = ta.propagate("NVDA", "2024-05-10")
+final_state, decision = ta.propagate("NVDA", "2024-05-10")
print(decision)
```
+#### Accessing Analysis Results
+
+The `propagate()` method returns both the final state and the decision. You can access individual reports from the final state:
+
+```python
+final_state, decision = ta.propagate("NVDA", "2024-05-10")
+
+# Access individual analyst reports
+market_report = final_state["market_report"]
+sentiment_report = final_state["sentiment_report"]
+news_report = final_state["news_report"]
+fundamentals_report = final_state["fundamentals_report"]
+
+# Access research team decisions
+investment_plan = final_state["investment_plan"]
+trader_plan = final_state["trader_investment_plan"]
+
+# Access final decision
+final_decision = final_state["final_trade_decision"]
+
+print(f"Decision: {decision}")
+print(f"Market Analysis: {market_report[:200]}...")
+```
+
+#### Saving Reports Programmatically
+
+You can also save the analysis results to files programmatically:
+
+```python
+import json
+from pathlib import Path
+
+# Save individual reports
+results_dir = Path("results/NVDA/2024-05-10/reports")
+results_dir.mkdir(parents=True, exist_ok=True)
+
+with open(results_dir / "market_report.md", "w") as f:
+ f.write(final_state["market_report"])
+
+# Save complete state as JSON
+with open(results_dir / "complete_analysis.json", "w") as f:
+ json.dump(final_state, f, indent=2)
+```
+
> For `online_tools`, we recommend enabling them for experimentation, as they provide access to real-time data. The agents' offline tools rely on cached data from our **Tauric TradingDB**, a curated dataset we use for backtesting. We're currently in the process of refining this dataset, and we plan to release it soon alongside our upcoming projects. Stay tuned!
You can view the full list of configurations in `tradingagents/default_config.py`.
diff --git a/cli/main.py b/cli/main.py
index 64616ee1..34fc5b52 100644
--- a/cli/main.py
+++ b/cli/main.py
@@ -1,6 +1,7 @@
from typing import Optional
import datetime
import typer
+import json
from pathlib import Path
from functools import wraps
from rich.console import Console
@@ -31,6 +32,7 @@ app = typer.Typer(
name="TradingAgents",
help="TradingAgents CLI: Multi-Agents LLM Financial Trading Framework",
add_completion=True, # Enable shell completion
+ invoke_without_command=True, # Allow running without specifying a command
)
@@ -99,7 +101,7 @@ class MessageBuffer:
if content is not None:
latest_section = section
latest_content = content
-
+
if latest_section and latest_content:
# Format the current section for display
section_titles = {
@@ -166,6 +168,111 @@ class MessageBuffer:
self.final_report = "\n\n".join(report_parts) if report_parts else None
+ def save_final_report(self, report_dir, ticker, analysis_date, format="markdown"):
+ """Save the final consolidated report to a file with comprehensive metadata.
+
+ Args:
+ report_dir: Directory to save the report
+ ticker: Stock ticker symbol
+ analysis_date: Date of analysis
+ format: Output format ("markdown" or "json")
+ """
+ if not self.final_report:
+ return None
+
+ if format.lower() == "json":
+ return self._save_json_report(report_dir, ticker, analysis_date)
+ else:
+ return self._save_markdown_report(report_dir, ticker, analysis_date)
+
+ def _save_markdown_report(self, report_dir, ticker, analysis_date):
+ """Save the final report in Markdown format."""
+ final_report_file = report_dir / "final_report.md"
+
+ # Create comprehensive report with metadata
+ report_content = f"""# Trading Analysis Report
+
+## Analysis Summary
+- **Ticker:** {ticker}
+- **Analysis Date:** {analysis_date}
+- **Generated:** {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
+- **Total Agents:** {len(self.agent_status)}
+- **Completed Agents:** {sum(1 for status in self.agent_status.values() if status == 'completed')}
+
+## Agent Status Summary
+"""
+
+ # Add agent status summary
+ teams = {
+ "Analyst Team": ["Market Analyst", "Social Analyst", "News Analyst", "Fundamentals Analyst"],
+ "Research Team": ["Bull Researcher", "Bear Researcher", "Research Manager"],
+ "Trading Team": ["Trader"],
+ "Risk Management": ["Risky Analyst", "Neutral Analyst", "Safe Analyst"],
+ "Portfolio Management": ["Portfolio Manager"],
+ }
+
+ for team, agents in teams.items():
+ report_content += f"\n### {team}\n"
+ for agent in agents:
+ status = self.agent_status.get(agent, "unknown")
+ status_icon = "✅" if status == "completed" else "⏳" if status == "in_progress" else "❌"
+ report_content += f"- {status_icon} {agent}: {status}\n"
+
+ report_content += f"\n---\n\n"
+ report_content += self.final_report
+
+ # Save to file
+ with open(final_report_file, "w", encoding="utf-8") as f:
+ f.write(report_content)
+
+ return final_report_file
+
+ def _save_json_report(self, report_dir, ticker, analysis_date):
+ """Save the final report in JSON format."""
+ final_report_file = report_dir / "final_report.json"
+
+ # Create JSON structure
+ report_data = {
+ "metadata": {
+ "ticker": ticker,
+ "analysis_date": analysis_date,
+ "generated": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ "total_agents": len(self.agent_status),
+ "completed_agents": sum(1 for status in self.agent_status.values() if status == 'completed')
+ },
+ "agent_status": self.agent_status,
+ "report_sections": self.report_sections,
+ "final_report": self.final_report,
+ "messages": [
+ {
+ "timestamp": timestamp,
+ "type": msg_type,
+ "content": content
+ }
+ for timestamp, msg_type, content in self.messages
+ ],
+ "tool_calls": [
+ {
+ "timestamp": timestamp,
+ "tool_name": tool_name,
+ "args": args
+ }
+ for timestamp, tool_name, args in self.tool_calls
+ ]
+ }
+
+ # Save to file
+ with open(final_report_file, "w", encoding="utf-8") as f:
+ json.dump(report_data, f, indent=2, ensure_ascii=False)
+
+ return final_report_file
+
+ def save_final_report_both_formats(self, report_dir, ticker, analysis_date):
+ """Save the final report in both Markdown and JSON formats."""
+ markdown_file = self._save_markdown_report(report_dir, ticker, analysis_date)
+ json_file = self._save_json_report(report_dir, ticker, analysis_date)
+ return markdown_file, json_file
+
message_buffer = MessageBuffer()
@@ -313,7 +420,7 @@ def update_display(layout, spinner_text=None):
content_str = ' '.join(text_parts)
elif not isinstance(content_str, str):
content_str = str(content)
-
+
# Truncate message content if too long
if len(content_str) > 200:
content_str = content_str[:197] + "..."
@@ -470,7 +577,7 @@ def get_user_selections():
)
)
selected_llm_provider, backend_url = select_llm_provider()
-
+
# Step 6: Thinking agents
console.print(
create_question_box(
@@ -731,7 +838,7 @@ def extract_content_string(content):
else:
return str(content)
-def run_analysis():
+def run_analysis(format="markdown", save_reports=False):
# First get all user selections
selections = get_user_selections()
@@ -767,7 +874,7 @@ def run_analysis():
with open(log_file, "a") as f:
f.write(f"{timestamp} [{message_type}] {content}\n")
return wrapper
-
+
def save_tool_call_decorator(obj, func_name):
func = getattr(obj, func_name)
@wraps(func)
@@ -857,7 +964,7 @@ def run_analysis():
msg_type = "System"
# Add message to buffer
- message_buffer.add_message(msg_type, content)
+ message_buffer.add_message(msg_type, content)
# If it's a tool call, add it to tool calls
if hasattr(last_message, "tool_calls"):
@@ -1093,12 +1200,105 @@ def run_analysis():
# Display the complete final report
display_complete_report(final_state)
+ # Save the final consolidated report to a file
+ if save_reports:
+ if format == "both":
+ markdown_file, json_file = message_buffer.save_final_report_both_formats(report_dir, selections["ticker"], selections["analysis_date"])
+ if markdown_file and json_file:
+ console.print(f"\n[bold green]Final reports saved to:[/bold green]")
+ console.print(f" 📄 Markdown: {markdown_file}")
+ console.print(f" 📊 JSON: {json_file}")
+ else:
+ final_report_file = message_buffer.save_final_report(report_dir, selections["ticker"], selections["analysis_date"], format=format)
+ if final_report_file:
+ console.print(f"\n[bold green]Final report saved to:[/bold green] {final_report_file}")
+
update_display(layout)
+# Set analyze as the default command
+def default_command(
+ format: str = typer.Option("both", "--format", "-f", help="Output format: markdown, json, or both"),
+ save_reports: bool = typer.Option(False, "--save-reports", "-s", help="Save final reports to files")
+):
+ """Run trading analysis (default command)."""
+ run_analysis(format=format, save_reports=save_reports)
+
+# Set the default command
+app.callback(invoke_without_command=True)(default_command)
+
+
@app.command()
-def analyze():
- run_analysis()
+def analyze(
+ format: str = typer.Option("both", "--format", "-f", help="Output format: markdown, json, or both"),
+ save_reports: bool = typer.Option(False, "--save-reports", "-s", help="Save final reports to files")
+):
+ """Run trading analysis with specified output format."""
+ run_analysis(format=format, save_reports=save_reports)
+
+
+@app.command()
+def save_report(
+ ticker: str = typer.Argument(..., help="Stock ticker symbol"),
+ analysis_date: str = typer.Argument(..., help="Analysis date (YYYY-MM-DD)"),
+ format: str = typer.Option("markdown", "--format", "-f", help="Output format: markdown, json, or both"),
+ results_dir: str = typer.Option("results", "--results-dir", "-r", help="Results directory")
+):
+ """Save final report from existing analysis data."""
+ # Construct the path to the existing analysis
+ analysis_path = Path(results_dir) / ticker / analysis_date
+ report_dir = analysis_path / "reports"
+
+ if not report_dir.exists():
+ console.print(f"[bold red]Error:[/bold red] No analysis found for {ticker} on {analysis_date}")
+ console.print(f"Expected path: {analysis_path}")
+ return
+
+ # Check if individual report files exist
+ required_files = ["market_report.md", "sentiment_report.md", "news_report.md", "fundamentals_report.md"]
+ missing_files = [f for f in required_files if not (report_dir / f).exists()]
+
+ if missing_files:
+ console.print(f"[bold red]Error:[/bold red] Missing required report files: {', '.join(missing_files)}")
+ return
+
+ # Reconstruct the final report from individual files
+ console.print(f"[bold blue]Reconstructing final report for {ticker} ({analysis_date})...[/bold blue]")
+
+ # Read individual report sections
+ report_sections = {}
+ for file_name in required_files:
+ section_name = file_name.replace(".md", "")
+ file_path = report_dir / file_name
+ if file_path.exists():
+ with open(file_path, "r", encoding="utf-8") as f:
+ report_sections[section_name] = f.read()
+
+ # Also try to read other report files
+ additional_files = ["investment_plan.md", "trader_investment_plan.md", "final_trade_decision.md"]
+ for file_name in additional_files:
+ section_name = file_name.replace(".md", "")
+ file_path = report_dir / file_name
+ if file_path.exists():
+ with open(file_path, "r", encoding="utf-8") as f:
+ report_sections[section_name] = f.read()
+
+ # Create a temporary message buffer to generate the final report
+ temp_buffer = MessageBuffer()
+ temp_buffer.report_sections = report_sections
+ temp_buffer._update_final_report()
+
+ # Save the final report
+ if format == "both":
+ markdown_file, json_file = temp_buffer.save_final_report_both_formats(report_dir, ticker, analysis_date)
+ if markdown_file and json_file:
+ console.print(f"\n[bold green]Final reports saved to:[/bold green]")
+ console.print(f" 📄 Markdown: {markdown_file}")
+ console.print(f" 📊 JSON: {json_file}")
+ else:
+ final_report_file = temp_buffer.save_final_report(report_dir, ticker, analysis_date, format=format)
+ if final_report_file:
+ console.print(f"\n[bold green]Final report saved to:[/bold green] {final_report_file}")
if __name__ == "__main__":