Local changes: Updated model training, removed debug instrumentation, and configuration improvements
This commit is contained in:
272
backend/api/reporting.py
Normal file
272
backend/api/reporting.py
Normal file
@@ -0,0 +1,272 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user