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 cli.models import AnalystType
|
||||
from cli.utils import *
|
||||
from cli.pdf_generator import generate_pdf_report
|
||||
|
||||
console = Console()
|
||||
|
||||
|
|
@ -1099,6 +1100,23 @@ def run_analysis():
|
|||
|
||||
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()
|
||||
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
|
||||
langchain_anthropic
|
||||
langchain-google-genai
|
||||
reportlab
|
||||
matplotlib
|
||||
|
|
|
|||
Loading…
Reference in New Issue