feat: Add PDF report generation with charts for trading analysis
- Add comprehensive PDF report generation with all analysis stages - Include market data visualizations: price chart, volume chart, and technical indicators (RSI/MACD) - Fix text parsing issues with proper HTML/markdown escaping - Add reportlab and matplotlib dependencies for PDF and chart generation - PDF reports saved automatically after analysis completion in results directory 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1122505cd3
commit
1f6256d346
|
|
@ -0,0 +1,248 @@
|
||||||
|
"""Chart generation utilities for PDF reports."""
|
||||||
|
import io
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def generate_stock_price_chart(ticker: str, analysis_date: str) -> Optional[io.BytesIO]:
|
||||||
|
"""
|
||||||
|
Generate a stock price chart with moving averages.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ticker: Stock ticker symbol
|
||||||
|
analysis_date: Analysis date string (YYYY-MM-DD)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BytesIO buffer containing the chart image, or None if generation fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import matplotlib.dates as mdates
|
||||||
|
import yfinance as yf
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
except ImportError:
|
||||||
|
print("Warning: matplotlib required for chart generation")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Parse the analysis date
|
||||||
|
end_date = datetime.strptime(analysis_date, '%Y-%m-%d')
|
||||||
|
# Get 90 days of data for context
|
||||||
|
start_date = end_date - timedelta(days=90)
|
||||||
|
|
||||||
|
# Fetch stock data
|
||||||
|
stock = yf.Ticker(ticker)
|
||||||
|
df = stock.history(start=start_date.strftime('%Y-%m-%d'),
|
||||||
|
end=end_date.strftime('%Y-%m-%d'))
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
print(f"Warning: No data available for {ticker}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Calculate moving averages
|
||||||
|
df['SMA_20'] = df['Close'].rolling(window=20).mean()
|
||||||
|
df['SMA_50'] = df['Close'].rolling(window=50).mean()
|
||||||
|
|
||||||
|
# Create the figure
|
||||||
|
fig, ax = plt.subplots(figsize=(10, 6))
|
||||||
|
|
||||||
|
# Plot price and moving averages
|
||||||
|
ax.plot(df.index, df['Close'], label='Close Price', linewidth=2, color='#2E86AB')
|
||||||
|
ax.plot(df.index, df['SMA_20'], label='20-day SMA', linewidth=1.5,
|
||||||
|
linestyle='--', color='#A23B72', alpha=0.8)
|
||||||
|
ax.plot(df.index, df['SMA_50'], label='50-day SMA', linewidth=1.5,
|
||||||
|
linestyle='--', color='#F18F01', alpha=0.8)
|
||||||
|
|
||||||
|
# Formatting
|
||||||
|
ax.set_title(f'{ticker} Stock Price - Last 90 Days', fontsize=14, fontweight='bold')
|
||||||
|
ax.set_xlabel('Date', fontsize=12)
|
||||||
|
ax.set_ylabel('Price ($)', fontsize=12)
|
||||||
|
ax.legend(loc='best', fontsize=10)
|
||||||
|
ax.grid(True, alpha=0.3)
|
||||||
|
|
||||||
|
# Format x-axis dates
|
||||||
|
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
|
||||||
|
ax.xaxis.set_major_locator(mdates.MonthLocator())
|
||||||
|
plt.xticks(rotation=45)
|
||||||
|
|
||||||
|
# Tight layout to prevent label cutoff
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
|
# Save to BytesIO buffer
|
||||||
|
buf = io.BytesIO()
|
||||||
|
plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
|
||||||
|
buf.seek(0)
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
return buf
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error generating stock price chart: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def generate_volume_chart(ticker: str, analysis_date: str) -> Optional[io.BytesIO]:
|
||||||
|
"""
|
||||||
|
Generate a trading volume chart.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ticker: Stock ticker symbol
|
||||||
|
analysis_date: Analysis date string (YYYY-MM-DD)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BytesIO buffer containing the chart image, or None if generation fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import matplotlib.dates as mdates
|
||||||
|
import yfinance as yf
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Parse the analysis date
|
||||||
|
end_date = datetime.strptime(analysis_date, '%Y-%m-%d')
|
||||||
|
start_date = end_date - timedelta(days=90)
|
||||||
|
|
||||||
|
# Fetch stock data
|
||||||
|
stock = yf.Ticker(ticker)
|
||||||
|
df = stock.history(start=start_date.strftime('%Y-%m-%d'),
|
||||||
|
end=end_date.strftime('%Y-%m-%d'))
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Create the figure
|
||||||
|
fig, ax = plt.subplots(figsize=(10, 4))
|
||||||
|
|
||||||
|
# Plot volume bars
|
||||||
|
colors = ['green' if close >= open_ else 'red'
|
||||||
|
for close, open_ in zip(df['Close'], df['Open'])]
|
||||||
|
ax.bar(df.index, df['Volume'], color=colors, alpha=0.6)
|
||||||
|
|
||||||
|
# Add average volume line
|
||||||
|
avg_volume = df['Volume'].mean()
|
||||||
|
ax.axhline(y=avg_volume, color='blue', linestyle='--',
|
||||||
|
linewidth=1.5, label=f'Avg Volume: {avg_volume:,.0f}', alpha=0.7)
|
||||||
|
|
||||||
|
# Formatting
|
||||||
|
ax.set_title(f'{ticker} Trading Volume - Last 90 Days', fontsize=14, fontweight='bold')
|
||||||
|
ax.set_xlabel('Date', fontsize=12)
|
||||||
|
ax.set_ylabel('Volume', fontsize=12)
|
||||||
|
ax.legend(loc='best', fontsize=10)
|
||||||
|
ax.grid(True, alpha=0.3, axis='y')
|
||||||
|
|
||||||
|
# Format y-axis to show volume in millions
|
||||||
|
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'{x/1e6:.1f}M'))
|
||||||
|
|
||||||
|
# Format x-axis dates
|
||||||
|
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
|
||||||
|
ax.xaxis.set_major_locator(mdates.MonthLocator())
|
||||||
|
plt.xticks(rotation=45)
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
|
# Save to BytesIO buffer
|
||||||
|
buf = io.BytesIO()
|
||||||
|
plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
|
||||||
|
buf.seek(0)
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
return buf
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error generating volume chart: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def generate_technical_indicators_chart(ticker: str, analysis_date: str) -> Optional[io.BytesIO]:
|
||||||
|
"""
|
||||||
|
Generate a chart showing RSI and MACD technical indicators.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ticker: Stock ticker symbol
|
||||||
|
analysis_date: Analysis date string (YYYY-MM-DD)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BytesIO buffer containing the chart image, or None if generation fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import matplotlib.dates as mdates
|
||||||
|
import yfinance as yf
|
||||||
|
import pandas as pd
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Parse the analysis date
|
||||||
|
end_date = datetime.strptime(analysis_date, '%Y-%m-%d')
|
||||||
|
start_date = end_date - timedelta(days=90)
|
||||||
|
|
||||||
|
# Fetch stock data
|
||||||
|
stock = yf.Ticker(ticker)
|
||||||
|
df = stock.history(start=start_date.strftime('%Y-%m-%d'),
|
||||||
|
end=end_date.strftime('%Y-%m-%d'))
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Calculate RSI
|
||||||
|
delta = df['Close'].diff()
|
||||||
|
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
|
||||||
|
loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
|
||||||
|
rs = gain / loss
|
||||||
|
df['RSI'] = 100 - (100 / (1 + rs))
|
||||||
|
|
||||||
|
# Calculate MACD
|
||||||
|
exp1 = df['Close'].ewm(span=12, adjust=False).mean()
|
||||||
|
exp2 = df['Close'].ewm(span=26, adjust=False).mean()
|
||||||
|
df['MACD'] = exp1 - exp2
|
||||||
|
df['Signal'] = df['MACD'].ewm(span=9, adjust=False).mean()
|
||||||
|
|
||||||
|
# Create subplots
|
||||||
|
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
|
||||||
|
|
||||||
|
# RSI plot
|
||||||
|
ax1.plot(df.index, df['RSI'], label='RSI', linewidth=2, color='#2E86AB')
|
||||||
|
ax1.axhline(y=70, color='red', linestyle='--', linewidth=1, alpha=0.7, label='Overbought (70)')
|
||||||
|
ax1.axhline(y=30, color='green', linestyle='--', linewidth=1, alpha=0.7, label='Oversold (30)')
|
||||||
|
ax1.fill_between(df.index, 30, 70, alpha=0.1, color='gray')
|
||||||
|
ax1.set_ylabel('RSI', fontsize=12)
|
||||||
|
ax1.set_title(f'{ticker} Technical Indicators', fontsize=14, fontweight='bold')
|
||||||
|
ax1.legend(loc='best', fontsize=10)
|
||||||
|
ax1.grid(True, alpha=0.3)
|
||||||
|
ax1.set_ylim(0, 100)
|
||||||
|
|
||||||
|
# MACD plot
|
||||||
|
ax2.plot(df.index, df['MACD'], label='MACD', linewidth=2, color='#2E86AB')
|
||||||
|
ax2.plot(df.index, df['Signal'], label='Signal', linewidth=2, color='#F18F01')
|
||||||
|
ax2.bar(df.index, df['MACD'] - df['Signal'], label='Histogram',
|
||||||
|
color='gray', alpha=0.3)
|
||||||
|
ax2.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
|
||||||
|
ax2.set_xlabel('Date', fontsize=12)
|
||||||
|
ax2.set_ylabel('MACD', fontsize=12)
|
||||||
|
ax2.legend(loc='best', fontsize=10)
|
||||||
|
ax2.grid(True, alpha=0.3)
|
||||||
|
|
||||||
|
# Format x-axis dates
|
||||||
|
ax2.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
|
||||||
|
ax2.xaxis.set_major_locator(mdates.MonthLocator())
|
||||||
|
plt.xticks(rotation=45)
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
|
# Save to BytesIO buffer
|
||||||
|
buf = io.BytesIO()
|
||||||
|
plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
|
||||||
|
buf.seek(0)
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
return buf
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error generating technical indicators chart: {e}")
|
||||||
|
return None
|
||||||
18
cli/main.py
18
cli/main.py
|
|
@ -28,6 +28,7 @@ from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||||
from tradingagents.default_config import DEFAULT_CONFIG
|
from tradingagents.default_config import DEFAULT_CONFIG
|
||||||
from cli.models import AnalystType
|
from cli.models import AnalystType
|
||||||
from cli.utils import *
|
from cli.utils import *
|
||||||
|
from cli.pdf_generator import generate_pdf_report
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
@ -1099,6 +1100,23 @@ def run_analysis():
|
||||||
|
|
||||||
update_display(layout)
|
update_display(layout)
|
||||||
|
|
||||||
|
# Generate PDF report after the Live display context ends
|
||||||
|
try:
|
||||||
|
console.print("\n[bold cyan]Generating PDF report...[/bold cyan]")
|
||||||
|
pdf_path = generate_pdf_report(
|
||||||
|
final_state,
|
||||||
|
selections["ticker"],
|
||||||
|
selections["analysis_date"],
|
||||||
|
results_dir
|
||||||
|
)
|
||||||
|
console.print(f"[bold green]PDF report saved to:[/bold green] {pdf_path}")
|
||||||
|
except ImportError as e:
|
||||||
|
console.print(f"[yellow]Warning: {e}[/yellow]")
|
||||||
|
console.print("[yellow]PDF generation skipped. Install reportlab to enable PDF reports:[/yellow]")
|
||||||
|
console.print("[dim]pip install reportlab[/dim]")
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"[red]Error generating PDF report: {e}[/red]")
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def analyze():
|
def analyze():
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,269 @@
|
||||||
|
"""PDF report generator for TradingAgents analysis results."""
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any
|
||||||
|
import io
|
||||||
|
from cli.chart_generator import (
|
||||||
|
generate_stock_price_chart,
|
||||||
|
generate_volume_chart,
|
||||||
|
generate_technical_indicators_chart
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_pdf_report(
|
||||||
|
final_state: Dict[str, Any],
|
||||||
|
ticker: str,
|
||||||
|
analysis_date: str,
|
||||||
|
output_path: Path,
|
||||||
|
) -> Path:
|
||||||
|
"""
|
||||||
|
Generate a comprehensive PDF report from the analysis results.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
final_state: The final state dictionary containing all analysis results
|
||||||
|
ticker: The stock ticker symbol
|
||||||
|
analysis_date: The analysis date
|
||||||
|
output_path: Path where the PDF should be saved
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to the generated PDF file
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from reportlab.lib.pagesizes import letter, A4
|
||||||
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||||
|
from reportlab.lib.units import inch
|
||||||
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
|
||||||
|
from reportlab.platypus import Table, TableStyle, Image
|
||||||
|
from reportlab.lib import colors
|
||||||
|
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError(
|
||||||
|
"reportlab is required for PDF generation. "
|
||||||
|
"Install it with: pip install reportlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create the PDF document
|
||||||
|
pdf_file = output_path / f"{ticker}_{analysis_date}_analysis_report.pdf"
|
||||||
|
doc = SimpleDocTemplate(
|
||||||
|
str(pdf_file),
|
||||||
|
pagesize=letter,
|
||||||
|
rightMargin=72,
|
||||||
|
leftMargin=72,
|
||||||
|
topMargin=72,
|
||||||
|
bottomMargin=18,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Container for the 'Flowable' objects
|
||||||
|
elements = []
|
||||||
|
|
||||||
|
# Define styles
|
||||||
|
styles = getSampleStyleSheet()
|
||||||
|
styles.add(ParagraphStyle(
|
||||||
|
name='CustomTitle',
|
||||||
|
parent=styles['Heading1'],
|
||||||
|
fontSize=24,
|
||||||
|
textColor=colors.HexColor('#1a1a1a'),
|
||||||
|
spaceAfter=30,
|
||||||
|
alignment=TA_CENTER,
|
||||||
|
))
|
||||||
|
styles.add(ParagraphStyle(
|
||||||
|
name='CustomHeading1',
|
||||||
|
parent=styles['Heading1'],
|
||||||
|
fontSize=18,
|
||||||
|
textColor=colors.HexColor('#2c3e50'),
|
||||||
|
spaceAfter=12,
|
||||||
|
spaceBefore=12,
|
||||||
|
))
|
||||||
|
styles.add(ParagraphStyle(
|
||||||
|
name='CustomHeading2',
|
||||||
|
parent=styles['Heading2'],
|
||||||
|
fontSize=14,
|
||||||
|
textColor=colors.HexColor('#34495e'),
|
||||||
|
spaceAfter=10,
|
||||||
|
spaceBefore=10,
|
||||||
|
))
|
||||||
|
styles.add(ParagraphStyle(
|
||||||
|
name='CustomBody',
|
||||||
|
parent=styles['BodyText'],
|
||||||
|
fontSize=10,
|
||||||
|
alignment=TA_JUSTIFY,
|
||||||
|
spaceAfter=12,
|
||||||
|
))
|
||||||
|
|
||||||
|
# Title Page
|
||||||
|
title = Paragraph(
|
||||||
|
f"TradingAgents Analysis Report<br/>{ticker}",
|
||||||
|
styles['CustomTitle']
|
||||||
|
)
|
||||||
|
elements.append(title)
|
||||||
|
elements.append(Spacer(1, 12))
|
||||||
|
|
||||||
|
# Metadata table
|
||||||
|
metadata = [
|
||||||
|
['Analysis Date:', analysis_date],
|
||||||
|
['Report Generated:', datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')],
|
||||||
|
['Ticker Symbol:', ticker],
|
||||||
|
]
|
||||||
|
|
||||||
|
t = Table(metadata, colWidths=[2*inch, 4*inch])
|
||||||
|
t.setStyle(TableStyle([
|
||||||
|
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
||||||
|
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
|
||||||
|
('FONTSIZE', (0, 0), (-1, -1), 10),
|
||||||
|
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
|
||||||
|
('TOPPADDING', (0, 0), (-1, -1), 6),
|
||||||
|
]))
|
||||||
|
elements.append(t)
|
||||||
|
elements.append(Spacer(1, 30))
|
||||||
|
|
||||||
|
# Generate and add charts
|
||||||
|
elements.append(PageBreak())
|
||||||
|
elements.append(Paragraph('Market Data Visualizations', styles['CustomHeading1']))
|
||||||
|
elements.append(Spacer(1, 12))
|
||||||
|
|
||||||
|
# Stock price chart
|
||||||
|
print("Generating stock price chart...")
|
||||||
|
price_chart = generate_stock_price_chart(ticker, analysis_date)
|
||||||
|
if price_chart:
|
||||||
|
img = Image(price_chart, width=6*inch, height=3.6*inch)
|
||||||
|
elements.append(img)
|
||||||
|
elements.append(Spacer(1, 20))
|
||||||
|
|
||||||
|
# Volume chart
|
||||||
|
print("Generating volume chart...")
|
||||||
|
volume_chart = generate_volume_chart(ticker, analysis_date)
|
||||||
|
if volume_chart:
|
||||||
|
img = Image(volume_chart, width=6*inch, height=2.4*inch)
|
||||||
|
elements.append(img)
|
||||||
|
elements.append(Spacer(1, 20))
|
||||||
|
|
||||||
|
# Technical indicators chart
|
||||||
|
print("Generating technical indicators chart...")
|
||||||
|
tech_chart = generate_technical_indicators_chart(ticker, analysis_date)
|
||||||
|
if tech_chart:
|
||||||
|
img = Image(tech_chart, width=6*inch, height=4.8*inch)
|
||||||
|
elements.append(img)
|
||||||
|
elements.append(Spacer(1, 20))
|
||||||
|
|
||||||
|
# Helper function to clean and escape text for PDF
|
||||||
|
def clean_text(text: str) -> str:
|
||||||
|
"""Clean text and properly escape for reportlab."""
|
||||||
|
# Remove markdown headers
|
||||||
|
text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE)
|
||||||
|
|
||||||
|
# Escape special XML/HTML characters first
|
||||||
|
text = text.replace('&', '&')
|
||||||
|
text = text.replace('<', '<')
|
||||||
|
text = text.replace('>', '>')
|
||||||
|
|
||||||
|
# Now we can safely add our formatting tags
|
||||||
|
# Convert markdown bold (**text** or __text__) to HTML bold
|
||||||
|
text = re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', text)
|
||||||
|
text = re.sub(r'__(.+?)__', r'<b>\1</b>', text)
|
||||||
|
|
||||||
|
# Convert markdown italic (*text* or _text_) to HTML italic
|
||||||
|
# Be careful not to match ** or __
|
||||||
|
text = re.sub(r'(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)', r'<i>\1</i>', text)
|
||||||
|
text = re.sub(r'(?<!_)_(?!_)(.+?)(?<!_)_(?!_)', r'<i>\1</i>', text)
|
||||||
|
|
||||||
|
# Remove any remaining problematic characters
|
||||||
|
text = text.replace('\r', '')
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
# Helper function to add section
|
||||||
|
def add_section(title: str, content: str):
|
||||||
|
if content:
|
||||||
|
elements.append(Paragraph(title, styles['CustomHeading1']))
|
||||||
|
elements.append(Spacer(1, 12))
|
||||||
|
|
||||||
|
# Clean the content
|
||||||
|
content = clean_text(content)
|
||||||
|
|
||||||
|
# Split content into paragraphs
|
||||||
|
paragraphs = content.split('\n\n')
|
||||||
|
for para in paragraphs:
|
||||||
|
para = para.strip()
|
||||||
|
if para:
|
||||||
|
# Replace single newlines with spaces, but preserve structure
|
||||||
|
para = para.replace('\n', ' ')
|
||||||
|
# Clean up multiple spaces
|
||||||
|
para = re.sub(r'\s+', ' ', para)
|
||||||
|
try:
|
||||||
|
elements.append(Paragraph(para, styles['CustomBody']))
|
||||||
|
except Exception as e:
|
||||||
|
# If there's still an error, add as plain text
|
||||||
|
print(f"Warning: Could not parse paragraph, adding as plain text: {e}")
|
||||||
|
elements.append(Paragraph(para.replace('<', '<').replace('>', '>'), styles['CustomBody']))
|
||||||
|
elements.append(Spacer(1, 20))
|
||||||
|
|
||||||
|
# I. Analyst Team Reports
|
||||||
|
elements.append(PageBreak())
|
||||||
|
elements.append(Paragraph('I. Analyst Team Reports', styles['CustomHeading1']))
|
||||||
|
elements.append(Spacer(1, 20))
|
||||||
|
|
||||||
|
if final_state.get('market_report'):
|
||||||
|
add_section('Market Analysis', final_state['market_report'])
|
||||||
|
|
||||||
|
if final_state.get('sentiment_report'):
|
||||||
|
add_section('Social Sentiment Analysis', final_state['sentiment_report'])
|
||||||
|
|
||||||
|
if final_state.get('news_report'):
|
||||||
|
add_section('News Analysis', final_state['news_report'])
|
||||||
|
|
||||||
|
if final_state.get('fundamentals_report'):
|
||||||
|
add_section('Fundamentals Analysis', final_state['fundamentals_report'])
|
||||||
|
|
||||||
|
# II. Research Team Reports
|
||||||
|
if final_state.get('investment_debate_state'):
|
||||||
|
elements.append(PageBreak())
|
||||||
|
elements.append(Paragraph('II. Research Team Decision', styles['CustomHeading1']))
|
||||||
|
elements.append(Spacer(1, 20))
|
||||||
|
|
||||||
|
debate_state = final_state['investment_debate_state']
|
||||||
|
|
||||||
|
if debate_state.get('bull_history'):
|
||||||
|
add_section('Bull Researcher Analysis', debate_state['bull_history'])
|
||||||
|
|
||||||
|
if debate_state.get('bear_history'):
|
||||||
|
add_section('Bear Researcher Analysis', debate_state['bear_history'])
|
||||||
|
|
||||||
|
if debate_state.get('judge_decision'):
|
||||||
|
add_section('Research Manager Decision', debate_state['judge_decision'])
|
||||||
|
|
||||||
|
# III. Trading Team Reports
|
||||||
|
if final_state.get('trader_investment_plan'):
|
||||||
|
elements.append(PageBreak())
|
||||||
|
elements.append(Paragraph('III. Trading Team Plan', styles['CustomHeading1']))
|
||||||
|
elements.append(Spacer(1, 20))
|
||||||
|
add_section('Trader Plan', final_state['trader_investment_plan'])
|
||||||
|
|
||||||
|
# IV. Risk Management Team Reports
|
||||||
|
if final_state.get('risk_debate_state'):
|
||||||
|
elements.append(PageBreak())
|
||||||
|
elements.append(Paragraph('IV. Risk Management Team Decision', styles['CustomHeading1']))
|
||||||
|
elements.append(Spacer(1, 20))
|
||||||
|
|
||||||
|
risk_state = final_state['risk_debate_state']
|
||||||
|
|
||||||
|
if risk_state.get('risky_history'):
|
||||||
|
add_section('Aggressive Analyst Analysis', risk_state['risky_history'])
|
||||||
|
|
||||||
|
if risk_state.get('safe_history'):
|
||||||
|
add_section('Conservative Analyst Analysis', risk_state['safe_history'])
|
||||||
|
|
||||||
|
if risk_state.get('neutral_history'):
|
||||||
|
add_section('Neutral Analyst Analysis', risk_state['neutral_history'])
|
||||||
|
|
||||||
|
# V. Portfolio Manager Decision
|
||||||
|
if risk_state.get('judge_decision'):
|
||||||
|
elements.append(PageBreak())
|
||||||
|
elements.append(Paragraph('V. Portfolio Manager Decision', styles['CustomHeading1']))
|
||||||
|
elements.append(Spacer(1, 20))
|
||||||
|
add_section('Final Decision', risk_state['judge_decision'])
|
||||||
|
|
||||||
|
# Build PDF
|
||||||
|
doc.build(elements)
|
||||||
|
|
||||||
|
return pdf_file
|
||||||
|
|
@ -24,3 +24,5 @@ rich
|
||||||
questionary
|
questionary
|
||||||
langchain_anthropic
|
langchain_anthropic
|
||||||
langchain-google-genai
|
langchain-google-genai
|
||||||
|
reportlab
|
||||||
|
matplotlib
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue