"""Ingestion router: trigger data fetches from the market data provider. Provides both a single-source OHLCV endpoint and a comprehensive fetch-all endpoint that collects OHLCV + sentiment + fundamentals in one call with per-source status reporting. """ from __future__ import annotations import logging from datetime import date from fastapi import APIRouter, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.dependencies import get_db, require_access from app.exceptions import ProviderError from app.models.user import User from app.providers.alpaca import AlpacaOHLCVProvider from app.providers.fmp import FMPFundamentalProvider from app.providers.gemini_sentiment import GeminiSentimentProvider from app.schemas.common import APIEnvelope from app.services import fundamental_service, ingestion_service, sentiment_service logger = logging.getLogger(__name__) router = APIRouter(tags=["ingestion"]) def _get_provider() -> AlpacaOHLCVProvider: """Build the OHLCV provider from current settings.""" if not settings.alpaca_api_key or not settings.alpaca_api_secret: raise ProviderError("Alpaca API credentials not configured") return AlpacaOHLCVProvider(settings.alpaca_api_key, settings.alpaca_api_secret) @router.post("/ingestion/fetch/{symbol}", response_model=APIEnvelope) async def fetch_symbol( symbol: str, start_date: date | None = Query(None, description="Start date (YYYY-MM-DD)"), end_date: date | None = Query(None, description="End date (YYYY-MM-DD)"), _user: User = Depends(require_access), db: AsyncSession = Depends(get_db), ): """Fetch all data sources for a ticker: OHLCV, sentiment, and fundamentals. Returns a per-source breakdown so the frontend can show exactly what succeeded and what failed. """ symbol_upper = symbol.strip().upper() sources: dict[str, dict] = {} # --- OHLCV --- try: provider = _get_provider() result = await ingestion_service.fetch_and_ingest( db, provider, symbol_upper, start_date, end_date ) sources["ohlcv"] = { "status": "ok" if result.status in ("complete", "partial") else "error", "records": result.records_ingested, "message": result.message, } except Exception as exc: logger.error("OHLCV fetch failed for %s: %s", symbol_upper, exc) sources["ohlcv"] = {"status": "error", "records": 0, "message": str(exc)} # --- Sentiment --- if settings.gemini_api_key: try: sent_provider = GeminiSentimentProvider( settings.gemini_api_key, settings.gemini_model ) data = await sent_provider.fetch_sentiment(symbol_upper) await sentiment_service.store_sentiment( db, symbol=symbol_upper, classification=data.classification, confidence=data.confidence, source=data.source, timestamp=data.timestamp, ) sources["sentiment"] = { "status": "ok", "classification": data.classification, "confidence": data.confidence, "message": None, } except Exception as exc: logger.error("Sentiment fetch failed for %s: %s", symbol_upper, exc) sources["sentiment"] = {"status": "error", "message": str(exc)} else: sources["sentiment"] = { "status": "skipped", "message": "Gemini API key not configured", } # --- Fundamentals --- if settings.fmp_api_key: try: fmp_provider = FMPFundamentalProvider(settings.fmp_api_key) fdata = await fmp_provider.fetch_fundamentals(symbol_upper) await fundamental_service.store_fundamental( db, symbol=symbol_upper, pe_ratio=fdata.pe_ratio, revenue_growth=fdata.revenue_growth, earnings_surprise=fdata.earnings_surprise, market_cap=fdata.market_cap, ) sources["fundamentals"] = {"status": "ok", "message": None} except Exception as exc: logger.error("Fundamentals fetch failed for %s: %s", symbol_upper, exc) sources["fundamentals"] = {"status": "error", "message": str(exc)} else: sources["fundamentals"] = { "status": "skipped", "message": "FMP API key not configured", } # Always return success — per-source breakdown tells the full story return APIEnvelope( status="success", data={"symbol": symbol_upper, "sources": sources}, error=None, )