"""Trade endpoints — paginated trade history and detail.""" from __future__ import annotations from datetime import datetime from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from services.api_gateway.auth.middleware import get_current_user from sqlalchemy import select, desc, func router = APIRouter(prefix="/api/trades", tags=["trades"]) @router.get("") async def list_trades( request: Request, _user: dict = Depends(get_current_user), ticker: str | None = Query(default=None), start_date: datetime | None = Query(default=None), end_date: datetime | None = Query(default=None), date_from: datetime | None = Query(default=None), date_to: datetime | None = Query(default=None), strategy: str | None = Query(default=None), profitable: bool | None = Query(default=None), page: int = Query(default=1, ge=1), per_page: int = Query(default=20, ge=1, le=100), page_size: int | None = Query(default=None, ge=1, le=100), ) -> dict: """Paginated trade history with optional filters.""" from shared.models.trading import Trade, Strategy # Accept both parameter naming conventions effective_per_page = page_size if page_size is not None else per_page effective_start = start_date or date_from effective_end = end_date or date_to db = request.app.state.db_session_factory async with db() as session: query = ( select(Trade, Strategy.name.label("strategy_name")) .outerjoin(Strategy, Trade.strategy_id == Strategy.id) .order_by(desc(Trade.created_at)) ) count_query = select(func.count()).select_from(Trade) # Apply filters if ticker: query = query.where(Trade.ticker == ticker.upper()) count_query = count_query.where(Trade.ticker == ticker.upper()) if effective_start: query = query.where(Trade.created_at >= effective_start) count_query = count_query.where(Trade.created_at >= effective_start) if effective_end: query = query.where(Trade.created_at <= effective_end) count_query = count_query.where(Trade.created_at <= effective_end) if strategy: # Filter by strategy name (already joined) query = query.where(Strategy.name == strategy) count_query = count_query.join( Strategy, Trade.strategy_id == Strategy.id ).where(Strategy.name == strategy) if profitable is not None: if profitable: query = query.where(Trade.pnl > 0) count_query = count_query.where(Trade.pnl > 0) else: query = query.where(Trade.pnl <= 0) count_query = count_query.where(Trade.pnl <= 0) # Pagination total = (await session.execute(count_query)).scalar() or 0 offset = (page - 1) * effective_per_page query = query.offset(offset).limit(effective_per_page) result = await session.execute(query) rows = result.all() return { "trades": [ { "id": str(t.id), "ticker": t.ticker, "side": t.side.value, "qty": t.qty, "price": t.price, "status": t.status.value, "pnl": t.pnl, "strategy_id": str(t.strategy_id) if t.strategy_id else None, "strategy_name": strategy_name, "signal_id": str(t.signal_id) if t.signal_id else None, "created_at": t.created_at.isoformat() if t.created_at else None, } for t, strategy_name in rows ], "total": total, "page": page, "page_size": effective_per_page, "per_page": effective_per_page, "pages": (total + effective_per_page - 1) // effective_per_page if effective_per_page else 0, } @router.get("/{trade_id}") async def get_trade( trade_id: UUID, request: Request, _user: dict = Depends(get_current_user), ) -> dict: """Single trade detail with linked signal and outcome.""" from shared.models.trading import Trade db = request.app.state.db_session_factory async with db() as session: trade = ( await session.execute(select(Trade).where(Trade.id == trade_id)) ).scalar_one_or_none() if trade is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Trade not found", ) return { "id": str(trade.id), "ticker": trade.ticker, "side": trade.side.value, "qty": trade.qty, "price": trade.price, "status": trade.status.value, "pnl": trade.pnl, "strategy_id": str(trade.strategy_id) if trade.strategy_id else None, "signal_id": str(trade.signal_id) if trade.signal_id else None, "created_at": trade.created_at.isoformat() if trade.created_at else None, }