"""Export functionality for portfolio analyses. Provides PDF and CSV export capabilities for analysis results. """ import io from typing import Dict, Any, List, Optional from decimal import Decimal from datetime import datetime import csv import logging from reportlab.lib import colors from reportlab.lib.pagesizes import letter, A4 from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, PageBreak from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import inch logger = logging.getLogger(__name__) def export_analysis_to_csv(analysis_results: Dict[str, Any]) -> str: """Export analysis results to CSV format. Args: analysis_results: Complete analysis results dictionary Returns: CSV string ready for download """ output = io.StringIO() writer = csv.writer(output) # Headers writer.writerow(["Portfolio Analysis Export"]) writer.writerow(["Generated:", datetime.now().isoformat()]) writer.writerow([]) # Holdings writer.writerow(["Portfolio Holdings"]) writer.writerow(["Ticker", "Quantity", "Market Value", "Weight %"]) holdings = analysis_results.get('holdings', []) for holding in holdings: ticker = holding.get('ticker', '') quantity = holding.get('quantity', 0) market_value = holding.get('market_value', 0) weight = holding.get('weight', 0) * 100 writer.writerow([ticker, quantity, f"£{market_value:,.2f}", f"{weight:.2f}%"]) writer.writerow([]) # Key Metrics writer.writerow(["Key Metrics"]) risk_analysis = analysis_results.get('risk_analysis', {}) risk_metrics = risk_analysis.get('risk_metrics', {}) writer.writerow(["Metric", "Value"]) writer.writerow(["Sharpe Ratio", risk_metrics.get('sharpe_ratio', 'N/A')]) volatility = risk_metrics.get('volatility_annual', 0) if isinstance(volatility, (int, float)): writer.writerow(["Volatility", f"{volatility*100:.2f}%"]) else: writer.writerow(["Volatility", str(volatility)]) var_95 = risk_analysis.get('var_95', {}) var_value = var_95.get('var_percentage', 'N/A') if isinstance(var_95, dict) else var_95 writer.writerow(["VaR (95%)", f"{var_value}%"]) cvar_95 = risk_analysis.get('cvar_95', {}) cvar_value = cvar_95.get('cvar_percentage', 'N/A') if isinstance(cvar_95, dict) else cvar_95 writer.writerow(["CVaR (95%)", f"{cvar_value}%"]) writer.writerow([]) # AI Synthesis writer.writerow(["AI Analysis"]) ai_synthesis = analysis_results.get('ai_synthesis', '') if ai_synthesis: # Split into lines for better CSV formatting for line in ai_synthesis.split('\n'): if line.strip(): writer.writerow([line.strip()]) writer.writerow([]) # Recommendations writer.writerow(["Recommendations"]) recommendations = analysis_results.get('recommendations', []) for i, rec in enumerate(recommendations, 1): writer.writerow([f"{i}.", rec]) return output.getvalue() def export_analysis_to_pdf(analysis_results: Dict[str, Any]) -> bytes: """Export analysis results to PDF format. Args: analysis_results: Complete analysis results dictionary Returns: PDF bytes ready for download """ buffer = io.BytesIO() doc = SimpleDocTemplate(buffer, pagesize=letter) story = [] styles = getSampleStyleSheet() # Title title_style = ParagraphStyle( 'CustomTitle', parent=styles['Heading1'], fontSize=24, textColor=colors.HexColor('#05478A'), spaceAfter=30, ) story.append(Paragraph("Portfolio Analysis Report", title_style)) story.append(Paragraph(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}", styles['Normal'])) story.append(Spacer(1, 0.5*inch)) # Holdings Table story.append(Paragraph("Portfolio Holdings", styles['Heading2'])) holdings = analysis_results.get('holdings', []) holdings_data = [["Ticker", "Quantity", "Market Value", "Weight %"]] for holding in holdings: ticker = holding.get('ticker', '') quantity = holding.get('quantity', 0) market_value = holding.get('market_value', 0) weight = holding.get('weight', 0) * 100 holdings_data.append([ ticker, f"{quantity:.2f}", f"£{market_value:,.2f}", f"{weight:.2f}%" ]) holdings_table = Table(holdings_data) holdings_table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#05478A')), ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, 0), 12), ('BOTTOMPADDING', (0, 0), (-1, 0), 12), ('BACKGROUND', (0, 1), (-1, -1), colors.beige), ('GRID', (0, 0), (-1, -1), 1, colors.black) ])) story.append(holdings_table) story.append(Spacer(1, 0.5*inch)) # Key Metrics story.append(Paragraph("Key Metrics", styles['Heading2'])) risk_analysis = analysis_results.get('risk_analysis', {}) risk_metrics = risk_analysis.get('risk_metrics', {}) metrics_data = [["Metric", "Value"]] metrics_data.append(["Sharpe Ratio", f"{risk_metrics.get('sharpe_ratio', 0):.3f}"]) volatility = risk_metrics.get('volatility_annual', 0) if isinstance(volatility, (int, float)): metrics_data.append(["Volatility", f"{volatility*100:.2f}%"]) else: metrics_data.append(["Volatility", str(volatility)]) var_95 = risk_analysis.get('var_95', {}) var_value = var_95.get('var_percentage', 0) if isinstance(var_95, dict) else var_95 metrics_data.append(["VaR (95%)", f"{var_value:.2f}%"]) cvar_95 = risk_analysis.get('cvar_95', {}) cvar_value = cvar_95.get('cvar_percentage', 0) if isinstance(cvar_95, dict) else cvar_95 metrics_data.append(["CVaR (95%)", f"{cvar_value:.2f}%"]) metrics_table = Table(metrics_data) metrics_table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#048CFC')), ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), ('ALIGN', (0, 0), (-1, -1), 'LEFT'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, 0), 12), ('BOTTOMPADDING', (0, 0), (-1, 0), 12), ('BACKGROUND', (0, 1), (-1, -1), colors.lightblue), ('GRID', (0, 0), (-1, -1), 1, colors.black) ])) story.append(metrics_table) story.append(Spacer(1, 0.5*inch)) # AI Synthesis story.append(Paragraph("AI Analysis", styles['Heading2'])) ai_synthesis = analysis_results.get('ai_synthesis', '') if ai_synthesis: # Clean and format the text paragraphs = ai_synthesis.split('\n\n') for para in paragraphs: if para.strip(): story.append(Paragraph(para.strip(), styles['Normal'])) story.append(Spacer(1, 0.1*inch)) # Recommendations story.append(Spacer(1, 0.3*inch)) story.append(Paragraph("Recommendations", styles['Heading2'])) recommendations = analysis_results.get('recommendations', []) for i, rec in enumerate(recommendations, 1): story.append(Paragraph(f"{i}. {rec}", styles['Normal'])) story.append(Spacer(1, 0.1*inch)) # Build PDF try: doc.build(story) except Exception as e: logger.error(f"Failed to build PDF: {e}") raise buffer.seek(0) return buffer.getvalue()