Spaces:
Running
on
Zero
Running
on
Zero
| """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() | |