feat: Implement comprehensive PDF report generation system
🎯 Major Features Added: - Automatic PDF generation after each analysis - Playwright-based PDF engine with WeasyPrint fallback - Cross-platform compatibility (no system dependencies) - Professional structured reports matching terminal output 📋 Report Structure: - I. Analyst Team Reports (Market/Social/News/Fundamentals) - II. Research Team Decision (Bull/Bear/Research Manager) - III. Trading Team Plan (Trader recommendations) - IV. Risk Management Team Decision (Risk analysts) - V. Portfolio Manager Decision (Final decision) 🏗️ Architecture: - New tradingagents/reports/ module with organized structure - generators/: PDF generation engines - formatters/: Report structure and formatting - converters/: Content conversion utilities - Comprehensive documentation and error handling 🔧 Technical Details: - Added playwright>=1.40.0 dependency - Integrated into CLI workflow (cli/main.py) - Rich HTML export with professional styling - Automatic file generation: analysis_report.pdf + .html backup - File sizes: ~900KB PDF, ~45KB HTML ✅ Benefits: - Zero manual intervention required - High-quality professional reports - Structured content preservation - Robust error handling and fallbacks
This commit is contained in:
parent
57db1c545d
commit
a7e69fbad8
33
cli/main.py
33
cli/main.py
|
|
@ -22,6 +22,7 @@ from rich.rule import Rule
|
|||
|
||||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||
from tradingagents.default_config import DEFAULT_CONFIG
|
||||
from tradingagents.reports import TradingReportPDFGenerator
|
||||
from cli.models import AnalystType
|
||||
from cli.utils import *
|
||||
|
||||
|
|
@ -1093,6 +1094,38 @@ def run_analysis():
|
|||
# Display the complete final report
|
||||
display_complete_report(final_state)
|
||||
|
||||
# Generate PDF report if enabled
|
||||
if config.get("pdf_generation", {}).get("auto_generate", True):
|
||||
try:
|
||||
pdf_generator = TradingReportPDFGenerator(config.get("pdf_generation", {}))
|
||||
if pdf_generator.is_available():
|
||||
# Prepare analysis data for PDF generation
|
||||
analysis_data = {
|
||||
"final_report": final_state.get("final_trade_decision", ""),
|
||||
"report_sections": {
|
||||
section: content for section, content in message_buffer.report_sections.items()
|
||||
if content is not None
|
||||
},
|
||||
"final_state": final_state # Pass complete state for structured formatting
|
||||
}
|
||||
|
||||
pdf_path = pdf_generator.generate_pdf(
|
||||
analysis_data,
|
||||
selections["ticker"],
|
||||
selections["analysis_date"]
|
||||
)
|
||||
|
||||
if pdf_path:
|
||||
console.print(f"\n📄 [bold green]PDF report generated:[/bold green] {pdf_path}")
|
||||
console.print(f"📄 [bold green]HTML backup saved:[/bold green] {pdf_path.replace('.pdf', '.html')}")
|
||||
else:
|
||||
console.print("\n⚠️ [yellow]PDF generation failed, but analysis completed successfully[/yellow]")
|
||||
else:
|
||||
console.print("\n⚠️ [yellow]PDF generation not available (dependencies not installed or disabled)[/yellow]")
|
||||
except Exception as e:
|
||||
console.print(f"\n⚠️ [yellow]PDF generation failed: {str(e)}[/yellow]")
|
||||
console.print("Analysis completed successfully, but PDF could not be generated.")
|
||||
|
||||
update_display(layout)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -24,3 +24,4 @@ rich
|
|||
questionary
|
||||
langchain_anthropic
|
||||
langchain-google-genai
|
||||
playwright>=1.40.0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
# TradingAgents Reports Module
|
||||
|
||||
This module provides comprehensive report generation capabilities for TradingAgents analysis results.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
tradingagents/reports/
|
||||
├── __init__.py # Main module interface
|
||||
├── README.md # This documentation
|
||||
├── generators/ # Report generators
|
||||
│ ├── __init__.py
|
||||
│ └── pdf_generator.py # PDF generation with Playwright/WeasyPrint
|
||||
├── formatters/ # Content formatters
|
||||
│ ├── __init__.py
|
||||
│ └── report_formatter.py # Structures analysis data for reports
|
||||
└── converters/ # Content converters
|
||||
├── __init__.py
|
||||
└── html_converter.py # Rich console to HTML conversion
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### 🎯 **Automatic PDF Generation**
|
||||
- Generates PDF reports automatically after each analysis
|
||||
- Uses Playwright as primary engine (cross-platform, no system dependencies)
|
||||
- WeasyPrint fallback for compatibility
|
||||
- Professional styling with proper sections and formatting
|
||||
|
||||
### 📋 **Structured Reports**
|
||||
Reports follow the exact terminal structure:
|
||||
1. **I. Analyst Team Reports** - Market/Social/News/Fundamentals analysts
|
||||
2. **II. Research Team Decision** - Bull/Bear/Research Manager analysis
|
||||
3. **III. Trading Team Plan** - Trader's strategic recommendations
|
||||
4. **IV. Risk Management Team Decision** - Risk analysts' perspectives
|
||||
5. **V. Portfolio Manager Decision** - Final investment decision
|
||||
|
||||
### 🎨 **Rich Formatting**
|
||||
- Color-coded sections matching terminal output
|
||||
- Professional panels and styling
|
||||
- Markdown content conversion
|
||||
- Responsive layout for different content types
|
||||
|
||||
## Usage
|
||||
|
||||
The reports module is automatically integrated into the main CLI workflow. No manual intervention required.
|
||||
|
||||
```python
|
||||
from tradingagents.reports import TradingReportPDFGenerator
|
||||
|
||||
# Used automatically in cli/main.py
|
||||
generator = TradingReportPDFGenerator()
|
||||
pdf_path = generator.generate_pdf(analysis_data, ticker, date)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
PDF generation can be configured in `tradingagents/default_config.py`:
|
||||
|
||||
```python
|
||||
"pdf_generation": {
|
||||
"enabled": True,
|
||||
"output_dir": "results",
|
||||
"page_format": "A4",
|
||||
"margin": "2cm",
|
||||
"font_family": "Arial, sans-serif",
|
||||
"font_size": "12pt",
|
||||
"auto_generate": True,
|
||||
}
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **playwright**: Primary PDF generation engine
|
||||
- **weasyprint**: Fallback PDF generation (optional)
|
||||
- **rich**: Terminal styling and content formatting
|
||||
- **jinja2**: HTML templating
|
||||
|
||||
## Output
|
||||
|
||||
Generated files are saved to:
|
||||
- `results/{ticker}/{date}/analysis_report.pdf` - Main PDF report
|
||||
- `results/{ticker}/{date}/analysis_report.html` - HTML backup
|
||||
|
||||
Example file sizes:
|
||||
- PDF: ~900KB (comprehensive analysis with all sections)
|
||||
- HTML: ~45KB (structured content backup)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
"""
|
||||
TradingAgents Reports Module
|
||||
|
||||
This module provides comprehensive report generation capabilities for TradingAgents analysis,
|
||||
including PDF generation, HTML conversion, and structured formatting.
|
||||
|
||||
Structure:
|
||||
- generators/: PDF and other report generators
|
||||
- formatters/: Report structure and formatting logic
|
||||
- converters/: Content conversion utilities
|
||||
"""
|
||||
|
||||
try:
|
||||
from .generators import TradingReportPDFGenerator
|
||||
from .formatters import ReportFormatter
|
||||
from .converters import RichToHTMLConverter
|
||||
__all__ = ['TradingReportPDFGenerator', 'ReportFormatter', 'RichToHTMLConverter']
|
||||
except ImportError as e:
|
||||
# Handle missing dependencies gracefully
|
||||
print(f"Warning: Could not import report components: {e}")
|
||||
__all__ = []
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
"""
|
||||
Content Converters
|
||||
|
||||
This module contains converters for transforming content between different formats.
|
||||
"""
|
||||
|
||||
from .html_converter import RichToHTMLConverter
|
||||
|
||||
__all__ = ['RichToHTMLConverter']
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
"""
|
||||
HTML Converter for Rich Console Output
|
||||
|
||||
This module converts Rich console output to clean HTML suitable for PDF generation.
|
||||
"""
|
||||
|
||||
from rich.console import Console
|
||||
from rich.text import Text
|
||||
import re
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class RichToHTMLConverter:
|
||||
"""Converts Rich console output to clean HTML for PDF generation."""
|
||||
|
||||
def __init__(self):
|
||||
self.console = Console(record=True, width=120)
|
||||
|
||||
def rich_to_html(self, console_output: str) -> str:
|
||||
"""
|
||||
Convert Rich console output to clean HTML.
|
||||
|
||||
Args:
|
||||
console_output: Raw console output with Rich formatting
|
||||
|
||||
Returns:
|
||||
Clean HTML string suitable for PDF generation
|
||||
"""
|
||||
# Create a console and export to HTML
|
||||
html_content = self.console.export_html(
|
||||
inline_styles=True,
|
||||
code_format=None
|
||||
)
|
||||
|
||||
# Clean and optimize for PDF
|
||||
return self.clean_html_for_pdf(html_content)
|
||||
|
||||
def clean_html_for_pdf(self, html_content: str) -> str:
|
||||
"""
|
||||
Clean HTML content for better PDF rendering.
|
||||
|
||||
Args:
|
||||
html_content: Raw HTML content from Rich export
|
||||
|
||||
Returns:
|
||||
Cleaned HTML content
|
||||
"""
|
||||
# Remove background colors that don't work well in PDF
|
||||
html_content = re.sub(r'background-color:\s*#[0-9a-fA-F]{6};?', '', html_content)
|
||||
|
||||
# Ensure good contrast for text
|
||||
html_content = re.sub(r'color:\s*#[0-9a-fA-F]{6};?', 'color: #333333;', html_content)
|
||||
|
||||
# Remove excessive margins and padding
|
||||
html_content = re.sub(r'margin:\s*\d+px;?', 'margin: 5px;', html_content)
|
||||
html_content = re.sub(r'padding:\s*\d+px;?', 'padding: 3px;', html_content)
|
||||
|
||||
return html_content
|
||||
|
||||
def apply_pdf_styles(self, html_content: str) -> str:
|
||||
"""
|
||||
Apply PDF-specific styles to HTML content.
|
||||
|
||||
Args:
|
||||
html_content: HTML content to style
|
||||
|
||||
Returns:
|
||||
HTML content with PDF-optimized styles
|
||||
"""
|
||||
pdf_styles = """
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-size: 12pt;
|
||||
line-height: 1.4;
|
||||
color: #333333;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
.panel {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
margin: 10px 0;
|
||||
padding: 15px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
.panel-title {
|
||||
font-weight: bold;
|
||||
font-size: 14pt;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.recommendation {
|
||||
font-weight: bold;
|
||||
font-size: 16pt;
|
||||
color: #27ae60;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
padding: 10px;
|
||||
border: 2px solid #27ae60;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.section-header {
|
||||
font-size: 18pt;
|
||||
font-weight: bold;
|
||||
color: #2980b9;
|
||||
margin: 30px 0 15px 0;
|
||||
border-bottom: 2px solid #2980b9;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 2cm;
|
||||
}
|
||||
.page-break {
|
||||
page-break-before: always;
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
|
||||
# Insert styles into HTML head
|
||||
if '<head>' in html_content:
|
||||
html_content = html_content.replace('<head>', f'<head>{pdf_styles}')
|
||||
else:
|
||||
html_content = f'<html><head>{pdf_styles}</head><body>{html_content}</body></html>'
|
||||
|
||||
return html_content
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
"""
|
||||
Report Formatters
|
||||
|
||||
This module contains formatters for structuring analysis data into various output formats.
|
||||
"""
|
||||
|
||||
from .report_formatter import ReportFormatter
|
||||
|
||||
__all__ = ['ReportFormatter']
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
"""
|
||||
Report Formatter for TradingAgents Analysis
|
||||
|
||||
This module formats and structures analysis results for PDF generation,
|
||||
matching the exact terminal output structure.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
|
||||
class ReportFormatter:
|
||||
"""Formats trading analysis results into structured sections for PDF generation."""
|
||||
|
||||
def __init__(self):
|
||||
self.sections = {}
|
||||
|
||||
def format_complete_report(self, analysis_data: Dict[str, Any], ticker: str, date: str) -> str:
|
||||
"""
|
||||
Format complete analysis report into structured HTML matching the terminal output.
|
||||
|
||||
Args:
|
||||
analysis_data: Complete analysis results including final_state
|
||||
ticker: Stock ticker symbol
|
||||
date: Analysis date
|
||||
|
||||
Returns:
|
||||
Formatted HTML content matching terminal structure
|
||||
"""
|
||||
html_sections = []
|
||||
|
||||
# Cover page
|
||||
html_sections.append(self._create_cover_page(ticker, date))
|
||||
|
||||
# Get final_state for structured data
|
||||
final_state = analysis_data.get('final_state', {})
|
||||
|
||||
# Complete Analysis Report Header
|
||||
html_sections.append('<h1 style="color: #00aa00; font-weight: bold; text-align: center; margin: 30px 0;">Complete Analysis Report</h1>')
|
||||
|
||||
# I. Analyst Team Reports
|
||||
analyst_section = self._format_analyst_team_reports(final_state)
|
||||
if analyst_section:
|
||||
html_sections.append(analyst_section)
|
||||
|
||||
# II. Research Team Decision
|
||||
research_section = self._format_research_team_decision(final_state)
|
||||
if research_section:
|
||||
html_sections.append(research_section)
|
||||
|
||||
# III. Trading Team Plan
|
||||
trading_section = self._format_trading_team_plan(final_state)
|
||||
if trading_section:
|
||||
html_sections.append(trading_section)
|
||||
|
||||
# IV. Risk Management Team Decision
|
||||
risk_section = self._format_risk_management_team_decision(final_state)
|
||||
if risk_section:
|
||||
html_sections.append(risk_section)
|
||||
|
||||
# V. Portfolio Manager Decision
|
||||
portfolio_section = self._format_portfolio_manager_decision(final_state)
|
||||
if portfolio_section:
|
||||
html_sections.append(portfolio_section)
|
||||
|
||||
return '\n'.join(html_sections)
|
||||
|
||||
def _create_cover_page(self, ticker: str, date: str) -> str:
|
||||
"""Create report cover page."""
|
||||
return f"""
|
||||
<div class="cover-page" style="page-break-after: always; text-align: center; margin-top: 100px;">
|
||||
<h1 style="font-size: 36px; color: #2c3e50; margin-bottom: 20px;">TradingAgents Analysis Report</h1>
|
||||
<h2 style="font-size: 28px; color: #3498db; margin-bottom: 40px;">{ticker}</h2>
|
||||
<p style="font-size: 18px; color: #7f8c8d; margin-bottom: 20px;">Analysis Date: {date}</p>
|
||||
<p style="font-size: 16px; color: #7f8c8d;">Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||
<div style="margin-top: 80px; padding: 20px; border: 2px solid #3498db; border-radius: 10px; background-color: #f8f9fa;">
|
||||
<h3 style="color: #2c3e50; margin-bottom: 15px;">Multi-Agents LLM Financial Trading Framework</h3>
|
||||
<p style="color: #7f8c8d; font-size: 14px;">
|
||||
Workflow: Analyst Team → Research Team → Trader → Risk Management → Portfolio Management
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
def _format_analyst_team_reports(self, final_state: Dict[str, Any]) -> str:
|
||||
"""Format I. Analyst Team Reports section."""
|
||||
analysts = []
|
||||
|
||||
# Market Analyst Report
|
||||
if final_state.get("market_report"):
|
||||
analysts.append(self._create_analyst_panel("Market Analyst", final_state["market_report"]))
|
||||
|
||||
# Social Analyst Report
|
||||
if final_state.get("sentiment_report"):
|
||||
analysts.append(self._create_analyst_panel("Social Analyst", final_state["sentiment_report"]))
|
||||
|
||||
# News Analyst Report
|
||||
if final_state.get("news_report"):
|
||||
analysts.append(self._create_analyst_panel("News Analyst", final_state["news_report"]))
|
||||
|
||||
# Fundamentals Analyst Report
|
||||
if final_state.get("fundamentals_report"):
|
||||
analysts.append(self._create_analyst_panel("Fundamentals Analyst", final_state["fundamentals_report"]))
|
||||
|
||||
if not analysts:
|
||||
return ""
|
||||
|
||||
return f"""
|
||||
<div class="section" style="margin: 30px 0; page-break-inside: avoid;">
|
||||
<h2 style="color: #00bcd4; border-bottom: 3px solid #00bcd4; padding-bottom: 10px; margin-bottom: 20px;">
|
||||
I. Analyst Team Reports
|
||||
</h2>
|
||||
<div class="analyst-reports" style="display: flex; flex-wrap: wrap; gap: 20px;">
|
||||
{''.join(analysts)}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
def _format_research_team_decision(self, final_state: Dict[str, Any]) -> str:
|
||||
"""Format II. Research Team Decision section."""
|
||||
if not final_state.get("investment_debate_state"):
|
||||
return ""
|
||||
|
||||
debate_state = final_state["investment_debate_state"]
|
||||
researchers = []
|
||||
|
||||
# Bull Researcher Analysis
|
||||
if debate_state.get("bull_history"):
|
||||
researchers.append(self._create_analyst_panel("Bull Researcher", debate_state["bull_history"]))
|
||||
|
||||
# Bear Researcher Analysis
|
||||
if debate_state.get("bear_history"):
|
||||
researchers.append(self._create_analyst_panel("Bear Researcher", debate_state["bear_history"]))
|
||||
|
||||
# Research Manager Decision
|
||||
if debate_state.get("judge_decision"):
|
||||
researchers.append(self._create_analyst_panel("Research Manager", debate_state["judge_decision"]))
|
||||
|
||||
if not researchers:
|
||||
return ""
|
||||
|
||||
return f"""
|
||||
<div class="section" style="margin: 30px 0; page-break-inside: avoid;">
|
||||
<h2 style="color: #e91e63; border-bottom: 3px solid #e91e63; padding-bottom: 10px; margin-bottom: 20px;">
|
||||
II. Research Team Decision
|
||||
</h2>
|
||||
<div class="research-reports" style="display: flex; flex-wrap: wrap; gap: 20px;">
|
||||
{''.join(researchers)}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
def _format_trading_team_plan(self, final_state: Dict[str, Any]) -> str:
|
||||
"""Format III. Trading Team Plan section."""
|
||||
if not final_state.get("trader_investment_plan"):
|
||||
return ""
|
||||
|
||||
return f"""
|
||||
<div class="section" style="margin: 30px 0; page-break-inside: avoid;">
|
||||
<h2 style="color: #ff9800; border-bottom: 3px solid #ff9800; padding-bottom: 10px; margin-bottom: 20px;">
|
||||
III. Trading Team Plan
|
||||
</h2>
|
||||
<div class="trading-plan">
|
||||
{self._create_analyst_panel("Trader", final_state["trader_investment_plan"])}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
def _format_risk_management_team_decision(self, final_state: Dict[str, Any]) -> str:
|
||||
"""Format IV. Risk Management Team Decision section."""
|
||||
if not final_state.get("risk_debate_state"):
|
||||
return ""
|
||||
|
||||
risk_state = final_state["risk_debate_state"]
|
||||
risk_analysts = []
|
||||
|
||||
# Aggressive (Risky) Analyst Analysis
|
||||
if risk_state.get("risky_history"):
|
||||
risk_analysts.append(self._create_analyst_panel("Aggressive Analyst", risk_state["risky_history"]))
|
||||
|
||||
# Conservative (Safe) Analyst Analysis
|
||||
if risk_state.get("safe_history"):
|
||||
risk_analysts.append(self._create_analyst_panel("Conservative Analyst", risk_state["safe_history"]))
|
||||
|
||||
# Neutral Analyst Analysis
|
||||
if risk_state.get("neutral_history"):
|
||||
risk_analysts.append(self._create_analyst_panel("Neutral Analyst", risk_state["neutral_history"]))
|
||||
|
||||
if not risk_analysts:
|
||||
return ""
|
||||
|
||||
return f"""
|
||||
<div class="section" style="margin: 30px 0; page-break-inside: avoid;">
|
||||
<h2 style="color: #f44336; border-bottom: 3px solid #f44336; padding-bottom: 10px; margin-bottom: 20px;">
|
||||
IV. Risk Management Team Decision
|
||||
</h2>
|
||||
<div class="risk-reports" style="display: flex; flex-wrap: wrap; gap: 20px;">
|
||||
{''.join(risk_analysts)}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
def _format_portfolio_manager_decision(self, final_state: Dict[str, Any]) -> str:
|
||||
"""Format V. Portfolio Manager Decision section."""
|
||||
if not final_state.get("risk_debate_state") or not final_state["risk_debate_state"].get("judge_decision"):
|
||||
return ""
|
||||
|
||||
decision = final_state["risk_debate_state"]["judge_decision"]
|
||||
|
||||
return f"""
|
||||
<div class="section" style="margin: 30px 0; page-break-inside: avoid;">
|
||||
<h2 style="color: #4caf50; border-bottom: 3px solid #4caf50; padding-bottom: 10px; margin-bottom: 20px;">
|
||||
V. Portfolio Manager Decision
|
||||
</h2>
|
||||
<div class="portfolio-decision">
|
||||
{self._create_analyst_panel("Portfolio Manager", decision)}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
def _create_analyst_panel(self, title: str, content: str) -> str:
|
||||
"""Create a styled panel for an analyst's report."""
|
||||
# Convert markdown-style content to HTML if needed
|
||||
formatted_content = self._format_markdown_content(content)
|
||||
|
||||
return f"""
|
||||
<div class="analyst-panel" style="
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
border: 2px solid #3498db;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 10px;
|
||||
background-color: #f8f9fa;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
">
|
||||
<h3 style="color: #3498db; margin-top: 0; margin-bottom: 15px; border-bottom: 1px solid #3498db; padding-bottom: 8px;">
|
||||
{title}
|
||||
</h3>
|
||||
<div class="content" style="line-height: 1.6; color: #2c3e50;">
|
||||
{formatted_content}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
def _format_markdown_content(self, content: str) -> str:
|
||||
"""Convert basic markdown formatting to HTML."""
|
||||
if not content:
|
||||
return ""
|
||||
|
||||
# Replace markdown headers
|
||||
content = content.replace('### ', '<h4>').replace('\n### ', '</h4>\n<h4>')
|
||||
content = content.replace('## ', '<h3>').replace('\n## ', '</h3>\n<h3>')
|
||||
content = content.replace('# ', '<h2>').replace('\n# ', '</h2>\n<h2>')
|
||||
|
||||
# Replace bold text
|
||||
import re
|
||||
content = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', content)
|
||||
|
||||
# Replace bullet points
|
||||
content = re.sub(r'\n\s*•\s+', '\n<li>', content)
|
||||
content = re.sub(r'\n\s*\*\s+', '\n<li>', content)
|
||||
content = re.sub(r'\n\s*-\s+', '\n<li>', content)
|
||||
|
||||
# Wrap consecutive list items in <ul> tags
|
||||
content = re.sub(r'(<li>.*?)(?=\n(?!<li>))', r'<ul>\1</ul>', content, flags=re.DOTALL)
|
||||
|
||||
# Replace line breaks with <br> for better formatting
|
||||
content = content.replace('\n\n', '<br><br>')
|
||||
content = content.replace('\n', '<br>')
|
||||
|
||||
# Clean up any unclosed headers
|
||||
if '<h' in content and not content.endswith(('</h2>', '</h3>', '</h4>')):
|
||||
content += '</h4>'
|
||||
|
||||
return content
|
||||
|
||||
def _combine_sections(self, sections: List[str]) -> str:
|
||||
"""Combine all sections into final HTML."""
|
||||
return '\n'.join(sections)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
"""
|
||||
PDF and Report Generators
|
||||
|
||||
This module contains various generators for creating reports in different formats.
|
||||
"""
|
||||
|
||||
try:
|
||||
from .pdf_generator import TradingReportPDFGenerator
|
||||
__all__ = ['TradingReportPDFGenerator']
|
||||
except ImportError as e:
|
||||
# Handle missing dependencies gracefully
|
||||
print(f"Warning: Could not import PDF generator: {e}")
|
||||
__all__ = []
|
||||
|
|
@ -0,0 +1,412 @@
|
|||
"""
|
||||
PDF Generator for TradingAgents Analysis Reports
|
||||
|
||||
This module generates PDF reports from analysis results using WeasyPrint.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
PLAYWRIGHT_AVAILABLE = True
|
||||
except (ImportError, OSError) as e:
|
||||
PLAYWRIGHT_AVAILABLE = False
|
||||
logging.warning(f"Playwright not available: {str(e)}. Trying WeasyPrint fallback.")
|
||||
|
||||
try:
|
||||
from weasyprint import HTML, CSS
|
||||
from weasyprint.text.fonts import FontConfiguration
|
||||
WEASYPRINT_AVAILABLE = True
|
||||
except (ImportError, OSError) as e:
|
||||
WEASYPRINT_AVAILABLE = False
|
||||
HTML = None
|
||||
CSS = None
|
||||
FontConfiguration = None
|
||||
if not PLAYWRIGHT_AVAILABLE:
|
||||
logging.warning(f"WeasyPrint also not available: {str(e)}. PDF generation will be disabled.")
|
||||
|
||||
from ..converters.html_converter import RichToHTMLConverter
|
||||
from ..formatters.report_formatter import ReportFormatter
|
||||
|
||||
|
||||
class TradingReportPDFGenerator:
|
||||
"""Generates PDF reports from TradingAgents analysis results."""
|
||||
|
||||
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
||||
"""
|
||||
Initialize PDF generator.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary for PDF generation
|
||||
"""
|
||||
self.config = config or self._default_config()
|
||||
self.html_converter = RichToHTMLConverter()
|
||||
self.report_formatter = ReportFormatter()
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
if not WEASYPRINT_AVAILABLE:
|
||||
self.logger.warning("WeasyPrint not available. PDF generation disabled.")
|
||||
|
||||
def _default_config(self) -> Dict[str, Any]:
|
||||
"""Get default PDF generation configuration."""
|
||||
return {
|
||||
"enabled": True,
|
||||
"output_dir": "results",
|
||||
"page_format": "A4",
|
||||
"margin": "2cm",
|
||||
"font_family": "Arial, sans-serif",
|
||||
"font_size": "12pt"
|
||||
}
|
||||
|
||||
def generate_pdf(self, analysis_results: Dict[str, Any], ticker: str,
|
||||
date: str, output_path: Optional[str] = None) -> Optional[str]:
|
||||
"""
|
||||
Generate PDF report from analysis results.
|
||||
|
||||
Args:
|
||||
analysis_results: Complete analysis results dictionary
|
||||
ticker: Stock ticker symbol
|
||||
date: Analysis date
|
||||
output_path: Optional custom output path
|
||||
|
||||
Returns:
|
||||
Path to generated PDF file, or None if generation failed
|
||||
"""
|
||||
if not self.is_available():
|
||||
self.logger.error("No PDF generation method available.")
|
||||
return None
|
||||
|
||||
if not self.config.get("enabled", True):
|
||||
self.logger.info("PDF generation disabled in configuration.")
|
||||
return None
|
||||
|
||||
try:
|
||||
# Generate output path if not provided
|
||||
if not output_path:
|
||||
output_path = self._generate_output_path(ticker, date)
|
||||
|
||||
# Ensure output directory exists
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
|
||||
# Format the report content
|
||||
html_content = self._create_html_report(analysis_results, ticker, date)
|
||||
|
||||
# Generate PDF using available method
|
||||
success = self._html_to_pdf(html_content, output_path)
|
||||
|
||||
if success:
|
||||
# Also save HTML backup
|
||||
html_path = output_path.replace('.pdf', '.html')
|
||||
with open(html_path, 'w', encoding='utf-8') as f:
|
||||
f.write(html_content)
|
||||
|
||||
self.logger.info(f"PDF report generated: {output_path}")
|
||||
return output_path
|
||||
else:
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to generate PDF report: {str(e)}")
|
||||
return None
|
||||
|
||||
def _generate_output_path(self, ticker: str, date: str) -> str:
|
||||
"""Generate output path for PDF file."""
|
||||
output_dir = Path(self.config["output_dir"]) / ticker / date
|
||||
return str(output_dir / "analysis_report.pdf")
|
||||
|
||||
def _create_html_report(self, analysis_results: Dict[str, Any],
|
||||
ticker: str, date: str) -> str:
|
||||
"""
|
||||
Create complete HTML report from analysis results.
|
||||
|
||||
Args:
|
||||
analysis_results: Analysis results dictionary
|
||||
ticker: Stock ticker symbol
|
||||
date: Analysis date
|
||||
|
||||
Returns:
|
||||
Complete HTML content for PDF generation
|
||||
"""
|
||||
# Format the main report content
|
||||
report_content = self.report_formatter.format_complete_report(
|
||||
analysis_results, ticker, date
|
||||
)
|
||||
|
||||
# Wrap in complete HTML document with styles
|
||||
html_template = f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TradingAgents Analysis: {ticker}</title>
|
||||
{self._get_pdf_styles()}
|
||||
</head>
|
||||
<body>
|
||||
{report_content}
|
||||
<div class="footer">
|
||||
<p>Generated by TradingAgents on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
return html_template
|
||||
|
||||
def _get_pdf_styles(self) -> str:
|
||||
"""Get CSS styles for PDF generation."""
|
||||
return """
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 2cm;
|
||||
@bottom-center {
|
||||
content: "Page " counter(page) " of " counter(pages);
|
||||
font-size: 10pt;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-size: 11pt;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.cover-page {
|
||||
text-align: center;
|
||||
padding: 100px 0;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.report-title {
|
||||
font-size: 28pt;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.ticker-symbol {
|
||||
font-size: 36pt;
|
||||
font-weight: bold;
|
||||
color: #3498db;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.analysis-date, .generated-date {
|
||||
font-size: 14pt;
|
||||
color: #666;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: 30px 0;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-size: 18pt;
|
||||
font-weight: bold;
|
||||
color: #2980b9;
|
||||
margin: 30px 0 15px 0;
|
||||
border-bottom: 2px solid #2980b9;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
margin: 15px 0;
|
||||
padding: 20px;
|
||||
background-color: #fafafa;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-weight: bold;
|
||||
font-size: 14pt;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.recommendation {
|
||||
font-weight: bold;
|
||||
font-size: 20pt;
|
||||
color: #27ae60;
|
||||
text-align: center;
|
||||
margin: 25px 0;
|
||||
padding: 15px;
|
||||
border: 3px solid #27ae60;
|
||||
border-radius: 10px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.page-break {
|
||||
page-break-before: always;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
font-size: 9pt;
|
||||
color: #666;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 10px 0;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.executive-content,
|
||||
.analyst-content,
|
||||
.research-content,
|
||||
.trading-content,
|
||||
.final-decision-content {
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
|
||||
def _html_to_pdf(self, html_content: str, output_path: str) -> bool:
|
||||
"""
|
||||
Convert HTML content to PDF using available method.
|
||||
|
||||
Args:
|
||||
html_content: HTML content to convert
|
||||
output_path: Output PDF file path
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
# Try Playwright first (preferred method)
|
||||
if PLAYWRIGHT_AVAILABLE:
|
||||
try:
|
||||
return self._html_to_pdf_playwright(html_content, output_path)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Playwright PDF generation failed: {str(e)}. Trying WeasyPrint fallback.")
|
||||
|
||||
# Fallback to WeasyPrint
|
||||
if WEASYPRINT_AVAILABLE:
|
||||
try:
|
||||
return self._html_to_pdf_weasyprint(html_content, output_path)
|
||||
except Exception as e:
|
||||
self.logger.error(f"WeasyPrint PDF generation also failed: {str(e)}")
|
||||
|
||||
return False
|
||||
|
||||
def _html_to_pdf_playwright(self, html_content: str, output_path: str) -> bool:
|
||||
"""
|
||||
Convert HTML content to PDF using Playwright.
|
||||
|
||||
Args:
|
||||
html_content: HTML content to convert
|
||||
output_path: Output PDF file path
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
|
||||
# Set content and wait for it to load
|
||||
page.set_content(html_content, wait_until='networkidle')
|
||||
|
||||
# Generate PDF with options
|
||||
page.pdf(
|
||||
path=output_path,
|
||||
format='A4',
|
||||
margin={
|
||||
'top': '2cm',
|
||||
'right': '2cm',
|
||||
'bottom': '2cm',
|
||||
'left': '2cm'
|
||||
},
|
||||
print_background=True,
|
||||
display_header_footer=True,
|
||||
header_template='<div style="font-size:10px; width:100%; text-align:center; color:#666;">TradingAgents Analysis Report</div>',
|
||||
footer_template='<div style="font-size:10px; width:100%; text-align:center; color:#666;"><span class="pageNumber"></span> / <span class="totalPages"></span></div>'
|
||||
)
|
||||
|
||||
browser.close()
|
||||
self.logger.info("PDF generated successfully using Playwright")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Playwright PDF generation failed: {str(e)}")
|
||||
return False
|
||||
|
||||
def _html_to_pdf_weasyprint(self, html_content: str, output_path: str) -> bool:
|
||||
"""
|
||||
Convert HTML content to PDF using WeasyPrint.
|
||||
|
||||
Args:
|
||||
html_content: HTML content to convert
|
||||
output_path: Output PDF file path
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Create font configuration
|
||||
font_config = FontConfiguration()
|
||||
|
||||
# Generate PDF
|
||||
html_doc = HTML(string=html_content)
|
||||
html_doc.write_pdf(
|
||||
output_path,
|
||||
font_config=font_config,
|
||||
optimize_images=True
|
||||
)
|
||||
|
||||
self.logger.info("PDF generated successfully using WeasyPrint")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"WeasyPrint conversion failed: {str(e)}")
|
||||
# Try without font configuration as fallback
|
||||
try:
|
||||
html_doc = HTML(string=html_content)
|
||||
html_doc.write_pdf(output_path)
|
||||
self.logger.info("PDF generated successfully using WeasyPrint (fallback mode)")
|
||||
return True
|
||||
except Exception as fallback_error:
|
||||
self.logger.error(f"WeasyPrint fallback also failed: {str(fallback_error)}")
|
||||
return False
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if PDF generation is available."""
|
||||
return (PLAYWRIGHT_AVAILABLE or WEASYPRINT_AVAILABLE) and self.config.get("enabled", True)
|
||||
Loading…
Reference in New Issue