first commit
This commit is contained in:
127
app/routers/ingestion.py
Normal file
127
app/routers/ingestion.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""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,
|
||||
)
|
||||
Reference in New Issue
Block a user