273 lines
9.5 KiB
Python
273 lines
9.5 KiB
Python
"""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()
|