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:
parent
a438acdbbd
commit
3329abd5c7
110
README.md
110
README.md
|
|
@ -126,10 +126,16 @@ export OPENAI_API_KEY=$YOUR_OPENAI_API_KEY
|
||||||
|
|
||||||
### CLI Usage
|
### 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
|
```bash
|
||||||
python -m cli.main
|
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.
|
You will see a screen where you can select your desired tickers, date, LLMs, research depth, etc.
|
||||||
|
|
||||||
<p align="center">
|
<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%;">
|
<img src="assets/cli/cli_transaction.png" width="100%" style="display: inline-block; margin: 0 2%;">
|
||||||
</p>
|
</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
|
## TradingAgents Package
|
||||||
|
|
||||||
### Implementation Details
|
### Implementation Details
|
||||||
|
|
@ -163,7 +223,7 @@ from tradingagents.default_config import DEFAULT_CONFIG
|
||||||
ta = TradingAgentsGraph(debug=True, config=DEFAULT_CONFIG.copy())
|
ta = TradingAgentsGraph(debug=True, config=DEFAULT_CONFIG.copy())
|
||||||
|
|
||||||
# forward propagate
|
# forward propagate
|
||||||
_, decision = ta.propagate("NVDA", "2024-05-10")
|
final_state, decision = ta.propagate("NVDA", "2024-05-10")
|
||||||
print(decision)
|
print(decision)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -184,10 +244,54 @@ config["online_tools"] = True # Use online tools or cached data
|
||||||
ta = TradingAgentsGraph(debug=True, config=config)
|
ta = TradingAgentsGraph(debug=True, config=config)
|
||||||
|
|
||||||
# forward propagate
|
# forward propagate
|
||||||
_, decision = ta.propagate("NVDA", "2024-05-10")
|
final_state, decision = ta.propagate("NVDA", "2024-05-10")
|
||||||
print(decision)
|
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!
|
> 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`.
|
You can view the full list of configurations in `tradingagents/default_config.py`.
|
||||||
|
|
|
||||||
216
cli/main.py
216
cli/main.py
|
|
@ -1,6 +1,7 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import datetime
|
import datetime
|
||||||
import typer
|
import typer
|
||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
@ -31,6 +32,7 @@ app = typer.Typer(
|
||||||
name="TradingAgents",
|
name="TradingAgents",
|
||||||
help="TradingAgents CLI: Multi-Agents LLM Financial Trading Framework",
|
help="TradingAgents CLI: Multi-Agents LLM Financial Trading Framework",
|
||||||
add_completion=True, # Enable shell completion
|
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:
|
if content is not None:
|
||||||
latest_section = section
|
latest_section = section
|
||||||
latest_content = content
|
latest_content = content
|
||||||
|
|
||||||
if latest_section and latest_content:
|
if latest_section and latest_content:
|
||||||
# Format the current section for display
|
# Format the current section for display
|
||||||
section_titles = {
|
section_titles = {
|
||||||
|
|
@ -166,6 +168,111 @@ class MessageBuffer:
|
||||||
|
|
||||||
self.final_report = "\n\n".join(report_parts) if report_parts else None
|
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()
|
message_buffer = MessageBuffer()
|
||||||
|
|
||||||
|
|
@ -313,7 +420,7 @@ def update_display(layout, spinner_text=None):
|
||||||
content_str = ' '.join(text_parts)
|
content_str = ' '.join(text_parts)
|
||||||
elif not isinstance(content_str, str):
|
elif not isinstance(content_str, str):
|
||||||
content_str = str(content)
|
content_str = str(content)
|
||||||
|
|
||||||
# Truncate message content if too long
|
# Truncate message content if too long
|
||||||
if len(content_str) > 200:
|
if len(content_str) > 200:
|
||||||
content_str = content_str[:197] + "..."
|
content_str = content_str[:197] + "..."
|
||||||
|
|
@ -470,7 +577,7 @@ def get_user_selections():
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
selected_llm_provider, backend_url = select_llm_provider()
|
selected_llm_provider, backend_url = select_llm_provider()
|
||||||
|
|
||||||
# Step 6: Thinking agents
|
# Step 6: Thinking agents
|
||||||
console.print(
|
console.print(
|
||||||
create_question_box(
|
create_question_box(
|
||||||
|
|
@ -731,7 +838,7 @@ def extract_content_string(content):
|
||||||
else:
|
else:
|
||||||
return str(content)
|
return str(content)
|
||||||
|
|
||||||
def run_analysis():
|
def run_analysis(format="markdown", save_reports=False):
|
||||||
# First get all user selections
|
# First get all user selections
|
||||||
selections = get_user_selections()
|
selections = get_user_selections()
|
||||||
|
|
||||||
|
|
@ -767,7 +874,7 @@ def run_analysis():
|
||||||
with open(log_file, "a") as f:
|
with open(log_file, "a") as f:
|
||||||
f.write(f"{timestamp} [{message_type}] {content}\n")
|
f.write(f"{timestamp} [{message_type}] {content}\n")
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
def save_tool_call_decorator(obj, func_name):
|
def save_tool_call_decorator(obj, func_name):
|
||||||
func = getattr(obj, func_name)
|
func = getattr(obj, func_name)
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
|
|
@ -857,7 +964,7 @@ def run_analysis():
|
||||||
msg_type = "System"
|
msg_type = "System"
|
||||||
|
|
||||||
# Add message to buffer
|
# 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 it's a tool call, add it to tool calls
|
||||||
if hasattr(last_message, "tool_calls"):
|
if hasattr(last_message, "tool_calls"):
|
||||||
|
|
@ -1093,12 +1200,105 @@ def run_analysis():
|
||||||
# Display the complete final report
|
# Display the complete final report
|
||||||
display_complete_report(final_state)
|
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)
|
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()
|
@app.command()
|
||||||
def analyze():
|
def analyze(
|
||||||
run_analysis()
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue