"""Reporting API endpoints for CSV and PDF export.""" from fastapi import APIRouter, HTTPException, Query, Body from fastapi.responses import StreamingResponse from typing import Optional, Dict, Any from datetime import datetime from sqlalchemy import select import io import csv import tempfile from pathlib import Path from src.core.database import Trade, get_database router = APIRouter() def get_csv_exporter(): """Get CSV exporter instance.""" from src.reporting.csv_exporter import get_csv_exporter as _get_csv_exporter return _get_csv_exporter() def get_pdf_generator(): """Get PDF generator instance.""" from src.reporting.pdf_generator import get_pdf_generator as _get_pdf_generator return _get_pdf_generator() @router.post("/backtest/csv") async def export_backtest_csv( results: Dict[str, Any] = Body(...), ): """Export backtest results as CSV.""" try: output = io.StringIO() writer = csv.writer(output) # Write header writer.writerow(['Metric', 'Value']) # Write metrics writer.writerow(['Total Return', f"{(results.get('total_return', 0) * 100):.2f}%"]) writer.writerow(['Sharpe Ratio', f"{results.get('sharpe_ratio', 0):.2f}"]) writer.writerow(['Sortino Ratio', f"{results.get('sortino_ratio', 0):.2f}"]) writer.writerow(['Max Drawdown', f"{(results.get('max_drawdown', 0) * 100):.2f}%"]) writer.writerow(['Win Rate', f"{(results.get('win_rate', 0) * 100):.2f}%"]) writer.writerow(['Total Trades', results.get('total_trades', 0)]) writer.writerow(['Final Value', f"${results.get('final_value', 0):.2f}"]) writer.writerow(['Initial Capital', f"${results.get('initial_capital', 0):.2f}"]) # Write trades if available if results.get('trades'): writer.writerow([]) writer.writerow(['Trades']) writer.writerow(['Timestamp', 'Side', 'Price', 'Quantity', 'Value']) for trade in results['trades']: writer.writerow([ trade.get('timestamp', ''), trade.get('side', ''), f"${trade.get('price', 0):.2f}", trade.get('quantity', 0), f"${(trade.get('price', 0) * trade.get('quantity', 0)):.2f}", ]) output.seek(0) return StreamingResponse( iter([output.getvalue()]), media_type="text/csv", headers={"Content-Disposition": f"attachment; filename=backtest_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"} ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/backtest/pdf") async def export_backtest_pdf( results: Dict[str, Any] = Body(...), ): """Export backtest results as PDF.""" try: pdf_generator = get_pdf_generator() # Create temporary file with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_file: tmp_path = Path(tmp_file.name) # Convert results to metrics format expected by PDF generator metrics = { 'total_return_percent': (results.get('total_return', 0) * 100), 'sharpe_ratio': results.get('sharpe_ratio', 0), 'sortino_ratio': results.get('sortino_ratio', 0), 'max_drawdown': results.get('max_drawdown', 0), 'win_rate': results.get('win_rate', 0), } # Generate PDF success = pdf_generator.generate_performance_report( tmp_path, metrics, "Backtest Report" ) if not success: raise HTTPException(status_code=500, detail="Failed to generate PDF") # Read PDF and return as stream with open(tmp_path, 'rb') as f: pdf_content = f.read() # Clean up tmp_path.unlink() return StreamingResponse( io.BytesIO(pdf_content), media_type="application/pdf", headers={"Content-Disposition": f"attachment; filename=backtest_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"} ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/trades/csv") async def export_trades_csv( start_date: Optional[str] = None, end_date: Optional[str] = None, paper_trading: bool = True, ): """Export trades as CSV.""" try: csv_exporter = get_csv_exporter() # Parse dates if provided start = datetime.fromisoformat(start_date) if start_date else None end = datetime.fromisoformat(end_date) if end_date else None # Create temporary file with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp_file: tmp_path = Path(tmp_file.name) # Export to file success = csv_exporter.export_trades( filepath=tmp_path, paper_trading=paper_trading, start_date=start, end_date=end ) if not success: raise HTTPException(status_code=500, detail="Failed to export trades") # Read and return with open(tmp_path, 'r') as f: csv_content = f.read() tmp_path.unlink() return StreamingResponse( iter([csv_content]), media_type="text/csv", headers={"Content-Disposition": f"attachment; filename=trades_{datetime.now().strftime('%Y%m%d')}.csv"} ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/portfolio/csv") async def export_portfolio_csv( paper_trading: bool = True, ): """Export portfolio as CSV.""" try: csv_exporter = get_csv_exporter() # Create temporary file with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp_file: tmp_path = Path(tmp_file.name) # Export to file success = csv_exporter.export_portfolio(filepath=tmp_path) if not success: raise HTTPException(status_code=500, detail="Failed to export portfolio") # Read and return with open(tmp_path, 'r') as f: csv_content = f.read() tmp_path.unlink() return StreamingResponse( iter([csv_content]), media_type="text/csv", headers={"Content-Disposition": f"attachment; filename=portfolio_{datetime.now().strftime('%Y%m%d')}.csv"} ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/tax/{method}") async def generate_tax_report( method: str, # fifo, lifo, specific_id symbol: Optional[str] = Query(None), year: Optional[int] = Query(None), paper_trading: bool = Query(True), ): """Generate tax report using specified method.""" try: if year is None: year = datetime.now().year tax_reporter = get_tax_reporter() if method == "fifo": if symbol: events = tax_reporter.generate_fifo_report(symbol, year, paper_trading) else: # Generate for all symbols events = [] # Get all symbols from trades db = get_database() async with db.get_session() as session: stmt = select(Trade.symbol).distinct() result = await session.execute(stmt) symbols = result.scalars().all() for sym in symbols: events.extend(tax_reporter.generate_fifo_report(sym, year, paper_trading)) elif method == "lifo": if symbol: events = tax_reporter.generate_lifo_report(symbol, year, paper_trading) else: events = [] db = get_database() async with db.get_session() as session: stmt = select(Trade.symbol).distinct() result = await session.execute(stmt) symbols = result.scalars().all() for sym in symbols: events.extend(tax_reporter.generate_lifo_report(sym, year, paper_trading)) else: raise HTTPException(status_code=400, detail=f"Unsupported tax method: {method}") # Generate CSV output = io.StringIO() writer = csv.writer(output) writer.writerow(['Date', 'Symbol', 'Quantity', 'Cost Basis', 'Proceeds', 'Gain/Loss', 'Buy Date']) for event in events: writer.writerow([ event.get('date', ''), event.get('symbol', ''), event.get('quantity', 0), event.get('cost_basis', 0), event.get('proceeds', 0), event.get('gain_loss', 0), event.get('buy_date', ''), ]) output.seek(0) return StreamingResponse( iter([output.getvalue()]), media_type="text/csv", headers={"Content-Disposition": f"attachment; filename=tax_report_{method}_{year}_{datetime.now().strftime('%Y%m%d')}.csv"} ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) def get_tax_reporter(): """Get tax reporter instance.""" from src.reporting.tax_reporter import get_tax_reporter as _get_tax_reporter return _get_tax_reporter()