""" User Data Router - Watchlist and Portfolio Management Provides CRUD operations for user-specific data """ from fastapi import APIRouter, HTTPException, Depends from pydantic import BaseModel, Field from typing import List, Optional, Dict, Any from datetime import datetime import json router = APIRouter(prefix="/api", tags=["user-data"]) # ============================================================================ # MODELS # ============================================================================ class WatchlistItem(BaseModel): symbol: str = Field(..., description="Cryptocurrency symbol") note: Optional[str] = Field("", description="User note") added_at: Optional[str] = None class WatchlistAddRequest(BaseModel): symbol: str note: Optional[str] = "" class WatchlistUpdateRequest(BaseModel): note: str class PortfolioHolding(BaseModel): id: Optional[int] = None symbol: str amount: float purchase_price: float purchase_date: Optional[str] = None notes: Optional[str] = "" class PortfolioAddRequest(BaseModel): symbol: str amount: float purchase_price: float purchase_date: Optional[str] = None notes: Optional[str] = "" class PortfolioUpdateRequest(BaseModel): amount: Optional[float] = None purchase_price: Optional[float] = None notes: Optional[str] = None # ============================================================================ # IN-MEMORY STORAGE (Replace with database in production) # ============================================================================ _watchlist_storage: Dict[str, WatchlistItem] = {} _portfolio_storage: Dict[int, PortfolioHolding] = {} _portfolio_id_counter = 1 # ============================================================================ # WATCHLIST ENDPOINTS # ============================================================================ @router.get("/watchlist") async def get_watchlist(): """ Get user's watchlist Returns: List of watched cryptocurrency symbols with notes """ try: items = list(_watchlist_storage.values()) return { "success": True, "data": [item.dict() for item in items], "count": len(items), "timestamp": datetime.utcnow().isoformat() } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/watchlist") async def add_to_watchlist(request: WatchlistAddRequest): """ Add symbol to watchlist Args: symbol: Cryptocurrency symbol note: Optional user note """ try: symbol = request.symbol.upper() if symbol in _watchlist_storage: raise HTTPException(status_code=400, detail=f"{symbol} already in watchlist") item = WatchlistItem( symbol=symbol, note=request.note, added_at=datetime.utcnow().isoformat() ) _watchlist_storage[symbol] = item return { "success": True, "message": f"{symbol} added to watchlist", "data": item.dict(), "timestamp": datetime.utcnow().isoformat() } except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.put("/watchlist/{symbol}") async def update_watchlist_item(symbol: str, request: WatchlistUpdateRequest): """ Update watchlist item note Args: symbol: Cryptocurrency symbol note: Updated note """ try: symbol = symbol.upper() if symbol not in _watchlist_storage: raise HTTPException(status_code=404, detail=f"{symbol} not in watchlist") _watchlist_storage[symbol].note = request.note return { "success": True, "message": f"{symbol} updated", "data": _watchlist_storage[symbol].dict(), "timestamp": datetime.utcnow().isoformat() } except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.delete("/watchlist/{symbol}") async def remove_from_watchlist(symbol: str): """ Remove symbol from watchlist Args: symbol: Cryptocurrency symbol """ try: symbol = symbol.upper() if symbol not in _watchlist_storage: raise HTTPException(status_code=404, detail=f"{symbol} not in watchlist") del _watchlist_storage[symbol] return { "success": True, "message": f"{symbol} removed from watchlist", "timestamp": datetime.utcnow().isoformat() } except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # ============================================================================ # PORTFOLIO ENDPOINTS # ============================================================================ @router.get("/portfolio/holdings") async def get_holdings(): """ Get all portfolio holdings Returns: List of cryptocurrency holdings with purchase info """ try: holdings = list(_portfolio_storage.values()) # Calculate current values (would fetch real prices in production) total_value = 0 total_invested = 0 for holding in holdings: invested = holding.amount * holding.purchase_price total_invested += invested # In production, fetch current price and calculate current value # For now, use purchase price total_value += invested return { "success": True, "data": [h.dict() for h in holdings], "summary": { "total_holdings": len(holdings), "total_invested": total_invested, "total_value": total_value, "profit_loss": total_value - total_invested, "profit_loss_percent": ((total_value - total_invested) / total_invested * 100) if total_invested > 0 else 0 }, "timestamp": datetime.utcnow().isoformat() } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.post("/portfolio/holdings") async def add_holding(request: PortfolioAddRequest): """ Add new holding to portfolio Args: symbol: Cryptocurrency symbol amount: Amount held purchase_price: Price at purchase purchase_date: Date of purchase notes: Optional notes """ global _portfolio_id_counter try: holding = PortfolioHolding( id=_portfolio_id_counter, symbol=request.symbol.upper(), amount=request.amount, purchase_price=request.purchase_price, purchase_date=request.purchase_date or datetime.utcnow().isoformat(), notes=request.notes ) _portfolio_storage[_portfolio_id_counter] = holding _portfolio_id_counter += 1 return { "success": True, "message": "Holding added", "data": holding.dict(), "timestamp": datetime.utcnow().isoformat() } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.put("/portfolio/holdings/{holding_id}") async def update_holding(holding_id: int, request: PortfolioUpdateRequest): """ Update existing holding Args: holding_id: Holding ID amount: Updated amount (optional) purchase_price: Updated purchase price (optional) notes: Updated notes (optional) """ try: if holding_id not in _portfolio_storage: raise HTTPException(status_code=404, detail="Holding not found") holding = _portfolio_storage[holding_id] if request.amount is not None: holding.amount = request.amount if request.purchase_price is not None: holding.purchase_price = request.purchase_price if request.notes is not None: holding.notes = request.notes return { "success": True, "message": "Holding updated", "data": holding.dict(), "timestamp": datetime.utcnow().isoformat() } except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.delete("/portfolio/holdings/{holding_id}") async def delete_holding(holding_id: int): """ Delete holding from portfolio Args: holding_id: Holding ID """ try: if holding_id not in _portfolio_storage: raise HTTPException(status_code=404, detail="Holding not found") del _portfolio_storage[holding_id] return { "success": True, "message": "Holding deleted", "timestamp": datetime.utcnow().isoformat() } except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/portfolio/performance") async def get_performance(): """ Get portfolio performance metrics Returns: Overall portfolio performance including profit/loss """ try: holdings = list(_portfolio_storage.values()) if not holdings: return { "success": True, "data": { "total_value": 0, "total_invested": 0, "profit_loss": 0, "profit_loss_percent": 0, "best_performer": None, "worst_performer": None, "holdings_count": 0 }, "timestamp": datetime.utcnow().isoformat() } total_invested = sum(h.amount * h.purchase_price for h in holdings) # In production, fetch current prices and calculate real values total_value = total_invested # Placeholder return { "success": True, "data": { "total_value": total_value, "total_invested": total_invested, "profit_loss": total_value - total_invested, "profit_loss_percent": ((total_value - total_invested) / total_invested * 100) if total_invested > 0 else 0, "holdings_count": len(holdings), "avg_purchase_value": total_invested / len(holdings) if holdings else 0 }, "timestamp": datetime.utcnow().isoformat() } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/portfolio/history") async def get_portfolio_history(days: int = 30): """ Get historical portfolio performance Args: days: Number of days of history to retrieve Returns: Daily portfolio values for the specified period """ try: # Placeholder - in production, fetch historical data return { "success": True, "data": [], "message": "Historical data not yet implemented", "timestamp": datetime.utcnow().isoformat() } except Exception as e: raise HTTPException(status_code=500, detail=str(e))