feat: add support for saving reports to file

This commit enables the CLI to generate and save analysis reports
to the results/ directory structure, providing persistent storage
of trading analysis outputs.
This commit is contained in:
xxmplus 2025-07-05 22:51:17 -07:00
parent a438acdbbd
commit 3329abd5c7
2 changed files with 315 additions and 11 deletions

110
README.md
View File

@ -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.
<p align="center">
@ -146,6 +152,60 @@ An interface will appear showing results as they load, letting you track the age
<img src="assets/cli/cli_transaction.png" width="100%" style="display: inline-block; margin: 0 2%;">
</p>
#### 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/<TICKER>/<ANALYSIS_DATE>/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`.

View File

@ -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__":