"""Fundamental data service. Stores fundamental data (P/E, revenue growth, earnings surprise, market cap) and marks the fundamental dimension score as stale on new data. """ from __future__ import annotations import logging from datetime import datetime, timezone from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.exceptions import NotFoundError from app.models.fundamental import FundamentalData from app.models.score import DimensionScore from app.models.ticker import Ticker logger = logging.getLogger(__name__) async def _get_ticker(db: AsyncSession, symbol: str) -> Ticker: """Look up a ticker by symbol.""" normalised = symbol.strip().upper() result = await db.execute(select(Ticker).where(Ticker.symbol == normalised)) ticker = result.scalar_one_or_none() if ticker is None: raise NotFoundError(f"Ticker not found: {normalised}") return ticker async def store_fundamental( db: AsyncSession, symbol: str, pe_ratio: float | None = None, revenue_growth: float | None = None, earnings_surprise: float | None = None, market_cap: float | None = None, ) -> FundamentalData: """Store or update fundamental data for a ticker. Keeps a single latest snapshot per ticker. On new data, marks the fundamental dimension score as stale (if one exists). """ ticker = await _get_ticker(db, symbol) # Check for existing record result = await db.execute( select(FundamentalData).where(FundamentalData.ticker_id == ticker.id) ) existing = result.scalar_one_or_none() now = datetime.now(timezone.utc) if existing is not None: existing.pe_ratio = pe_ratio existing.revenue_growth = revenue_growth existing.earnings_surprise = earnings_surprise existing.market_cap = market_cap existing.fetched_at = now record = existing else: record = FundamentalData( ticker_id=ticker.id, pe_ratio=pe_ratio, revenue_growth=revenue_growth, earnings_surprise=earnings_surprise, market_cap=market_cap, fetched_at=now, ) db.add(record) # Mark fundamental dimension score as stale if it exists # TODO: Use DimensionScore service when built dim_result = await db.execute( select(DimensionScore).where( DimensionScore.ticker_id == ticker.id, DimensionScore.dimension == "fundamental", ) ) dim_score = dim_result.scalar_one_or_none() if dim_score is not None: dim_score.is_stale = True await db.commit() await db.refresh(record) return record async def get_fundamental( db: AsyncSession, symbol: str, ) -> FundamentalData | None: """Get the latest fundamental data for a ticker.""" ticker = await _get_ticker(db, symbol) result = await db.execute( select(FundamentalData).where(FundamentalData.ticker_id == ticker.id) ) return result.scalar_one_or_none()