""" Enhanced analytics API endpoints with efficient market data handling. This version uses PerformanceCalculatorV2 with: - Database-backed price caching - Rate-limited API calls - Stale-while-revalidate pattern for better UX """ from fastapi import APIRouter, Depends, Query, BackgroundTasks from sqlalchemy.orm import Session from typing import Optional from datetime import date from app.api.deps import get_db from app.services.performance_calculator_v2 import PerformanceCalculatorV2 from app.services.market_data_service import MarketDataService router = APIRouter() @router.get("/overview/{account_id}") def get_overview( account_id: int, refresh_prices: bool = Query(default=False, description="Force fresh price fetch"), max_api_calls: int = Query(default=5, ge=0, le=50, description="Max Yahoo Finance API calls"), start_date: Optional[date] = None, end_date: Optional[date] = None, db: Session = Depends(get_db) ): """ Get overview statistics for an account. By default, uses cached prices (stale-while-revalidate pattern). Set refresh_prices=true to force fresh data (may be slow). Args: account_id: Account ID refresh_prices: Whether to fetch fresh prices from Yahoo Finance max_api_calls: Maximum number of API calls to make start_date: Filter positions opened on or after this date end_date: Filter positions opened on or before this date db: Database session Returns: Dictionary with performance metrics and cache stats """ calculator = PerformanceCalculatorV2(db, cache_ttl=300) # If not refreshing, use cached only (fast) if not refresh_prices: max_api_calls = 0 stats = calculator.calculate_account_stats( account_id, update_prices=True, max_api_calls=max_api_calls, start_date=start_date, end_date=end_date ) return stats @router.get("/balance-history/{account_id}") def get_balance_history( account_id: int, days: int = Query(default=30, ge=1, le=3650), db: Session = Depends(get_db), ): """ Get account balance history for charting. This endpoint doesn't need market data, so it's always fast. Args: account_id: Account ID days: Number of days to retrieve (default: 30) db: Database session Returns: List of {date, balance} dictionaries """ calculator = PerformanceCalculatorV2(db) history = calculator.get_balance_history(account_id, days) return {"data": history} @router.get("/top-trades/{account_id}") def get_top_trades( account_id: int, limit: int = Query(default=10, ge=1, le=100), start_date: Optional[date] = None, end_date: Optional[date] = None, db: Session = Depends(get_db), ): """ Get top performing trades. This endpoint only uses closed positions, so no market data needed. Args: account_id: Account ID limit: Maximum number of trades to return (default: 10) start_date: Filter positions closed on or after this date end_date: Filter positions closed on or before this date db: Database session Returns: List of trade dictionaries """ calculator = PerformanceCalculatorV2(db) trades = calculator.get_top_trades(account_id, limit, start_date, end_date) return {"data": trades} @router.get("/worst-trades/{account_id}") def get_worst_trades( account_id: int, limit: int = Query(default=10, ge=1, le=100), start_date: Optional[date] = None, end_date: Optional[date] = None, db: Session = Depends(get_db), ): """ Get worst performing trades. This endpoint only uses closed positions, so no market data needed. Args: account_id: Account ID limit: Maximum number of trades to return (default: 10) start_date: Filter positions closed on or after this date end_date: Filter positions closed on or before this date db: Database session Returns: List of trade dictionaries """ calculator = PerformanceCalculatorV2(db) trades = calculator.get_worst_trades(account_id, limit, start_date, end_date) return {"data": trades} @router.post("/refresh-prices/{account_id}") def refresh_prices( account_id: int, max_api_calls: int = Query(default=10, ge=1, le=50), db: Session = Depends(get_db), ): """ Manually trigger a price refresh for open positions. This is useful when you want fresh data but don't want to wait on the dashboard load. Args: account_id: Account ID max_api_calls: Maximum number of Yahoo Finance API calls db: Database session Returns: Update statistics """ calculator = PerformanceCalculatorV2(db, cache_ttl=300) stats = calculator.update_open_positions_pnl( account_id, max_api_calls=max_api_calls, allow_stale=False # Force fresh fetches ) return { "message": "Price refresh completed", "stats": stats } @router.post("/refresh-prices-background/{account_id}") def refresh_prices_background( account_id: int, background_tasks: BackgroundTasks, max_api_calls: int = Query(default=20, ge=1, le=50), db: Session = Depends(get_db), ): """ Trigger a background price refresh. This returns immediately while prices are fetched in the background. Client can poll /overview endpoint to see updated data. Args: account_id: Account ID background_tasks: FastAPI background tasks max_api_calls: Maximum number of Yahoo Finance API calls db: Database session Returns: Acknowledgment that background task was started """ def refresh_task(): calculator = PerformanceCalculatorV2(db, cache_ttl=300) calculator.update_open_positions_pnl( account_id, max_api_calls=max_api_calls, allow_stale=False ) background_tasks.add_task(refresh_task) return { "message": "Price refresh started in background", "account_id": account_id, "max_api_calls": max_api_calls } @router.post("/refresh-stale-cache") def refresh_stale_cache( min_age_minutes: int = Query(default=10, ge=1, le=1440), limit: int = Query(default=20, ge=1, le=100), db: Session = Depends(get_db), ): """ Background maintenance endpoint to refresh stale cached prices. This can be called periodically (e.g., via cron) to keep cache fresh. Args: min_age_minutes: Only refresh prices older than this many minutes limit: Maximum number of prices to refresh db: Database session Returns: Number of prices refreshed """ market_data = MarketDataService(db, cache_ttl_seconds=300) refreshed = market_data.refresh_stale_prices( min_age_seconds=min_age_minutes * 60, limit=limit ) return { "message": "Stale price refresh completed", "refreshed": refreshed, "min_age_minutes": min_age_minutes } @router.delete("/clear-old-cache") def clear_old_cache( older_than_days: int = Query(default=30, ge=1, le=365), db: Session = Depends(get_db), ): """ Clear old cached prices from database. Args: older_than_days: Delete prices older than this many days db: Database session Returns: Number of records deleted """ market_data = MarketDataService(db) deleted = market_data.clear_cache(older_than_days=older_than_days) return { "message": "Old cache cleared", "deleted": deleted, "older_than_days": older_than_days }