BrianIsaac's picture
feat: implement P1 UX improvements - export, validation, and accessibility
5ab6829
"""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()