import uuid from datetime import date from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.ext.asyncio import AsyncSession from app.dependencies import get_current_user, get_db from app.db.models.user import User from app.schemas.investment import ( AssetSearch, AssetPricePoint, HoldingCreate, HoldingResponse, InvestmentTxnCreate, InvestmentTxnResponse, PerformanceMetrics, PortfolioSummary, ) from app.services import investment_service from app.services.price_feed_service import search_yahoo, fetch_history router = APIRouter(tags=["investments"]) # ── Portfolio ────────────────────────────────────────────────────────────── @router.get("/investments/portfolio", response_model=PortfolioSummary) async def get_portfolio( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): return await investment_service.get_portfolio(db, current_user.id) @router.get("/investments/performance", response_model=PerformanceMetrics) async def get_performance( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): return await investment_service.get_performance(db, current_user.id) # ── Holdings ─────────────────────────────────────────────────────────────── @router.post("/investments/holdings", response_model=HoldingResponse, status_code=status.HTTP_201_CREATED) async def create_holding( data: HoldingCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): from app.db.models.asset import Asset from sqlalchemy import select asset_result = await db.execute(select(Asset).where(Asset.id == data.asset_id)) asset = asset_result.scalar_one_or_none() if not asset: raise HTTPException(status_code=404, detail="Asset not found") holding = await investment_service.create_holding(db, current_user.id, data) await db.commit() await db.refresh(holding) return investment_service._holding_to_response(holding, asset) @router.delete("/investments/holdings/{holding_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_holding( holding_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): holding = await investment_service.get_holding(db, current_user.id, holding_id) if not holding: raise HTTPException(status_code=404, detail="Holding not found") await db.delete(holding) await db.commit() # ── Investment transactions ──────────────────────────────────────────────── @router.get("/investments/holdings/{holding_id}/transactions", response_model=list[InvestmentTxnResponse]) async def list_transactions( holding_id: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): return await investment_service.list_investment_transactions(db, current_user.id, holding_id) @router.post("/investments/transactions", response_model=InvestmentTxnResponse, status_code=status.HTTP_201_CREATED) async def add_transaction( data: InvestmentTxnCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): try: txn = await investment_service.add_investment_transaction(db, current_user.id, data) await db.commit() return txn except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) # ── Assets ───────────────────────────────────────────────────────────────── @router.get("/assets/search", response_model=list[AssetSearch]) async def search_assets( q: str = Query(..., min_length=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): # First search the local DB local = await investment_service.search_assets(db, q) if local: from app.db.models.asset import Asset return [AssetSearch( id=a.id, symbol=a.symbol, name=a.name, type=a.type, currency=a.currency, exchange=a.exchange, last_price=a.last_price, price_change_24h=a.price_change_24h, data_source=a.data_source, ) for a in local] # Fall back to live Yahoo search import asyncio loop = asyncio.get_event_loop() results = await loop.run_in_executor(None, search_yahoo, q) if not results: return [] # Upsert into DB so future searches are local created = [] for r in results: asset = await investment_service.get_or_create_asset( db, r["symbol"], r["name"], r["type"], r["currency"], r["data_source"], r.get("data_source_id"), r.get("exchange"), ) created.append(asset) await db.commit() return [AssetSearch( id=a.id, symbol=a.symbol, name=a.name, type=a.type, currency=a.currency, exchange=a.exchange, last_price=a.last_price, price_change_24h=a.price_change_24h, data_source=a.data_source, ) for a in created] @router.get("/assets/{asset_id}/prices", response_model=list[AssetPricePoint]) async def get_price_history( asset_id: uuid.UUID, days: int = Query(default=365, ge=7, le=1825), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): from app.db.models.asset import Asset from sqlalchemy import select asset_result = await db.execute(select(Asset).where(Asset.id == asset_id)) asset = asset_result.scalar_one_or_none() if not asset: raise HTTPException(status_code=404, detail="Asset not found") # Fetch from DB; if sparse, refresh from Yahoo prices = await investment_service.get_price_history(db, asset_id, days) if len(prices) < 5 and asset.data_source == "yahoo_finance": rows = await fetch_history(asset.symbol, days) if rows: await investment_service.upsert_price_history(db, asset_id, rows) await db.commit() prices = await investment_service.get_price_history(db, asset_id, days) return [ AssetPricePoint( date=p.date, open=p.open, high=p.high, low=p.low, close=p.close, volume=p.volume, ) for p in prices ] @router.post("/assets", response_model=AssetSearch, status_code=status.HTTP_201_CREATED) async def create_asset( symbol: str, name: str, asset_type: str = "stock", currency: str = "GBP", data_source: str = "yahoo_finance", data_source_id: str | None = None, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): asset = await investment_service.get_or_create_asset( db, symbol, name, asset_type, currency, data_source, data_source_id ) await db.commit() return AssetSearch( id=asset.id, symbol=asset.symbol, name=asset.name, type=asset.type, currency=asset.currency, exchange=asset.exchange, last_price=asset.last_price, price_change_24h=asset.price_change_24h, data_source=asset.data_source, )