Initial commit: Crypto trader application

This commit is contained in:
2025-12-25 20:20:40 -05:00
commit 07a04c1bb8
47895 changed files with 2042266 additions and 0 deletions

272
backend/api/reporting.py Normal file
View 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()