|
|
"""
|
|
|
Earnings Surprise endpoints — SEC EDGAR XBRL EPS data
|
|
|
"""
|
|
|
|
|
|
import logging
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
|
from fastapi.responses import Response
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
from app.core.database import get_db
|
|
|
from app.schemas.earnings import EarningsSurpriseEntry, EarningsSurpriseResponse
|
|
|
from app.services.earnings_service import EarningsService
|
|
|
from app.utils.cache import with_cache
|
|
|
|
|
|
router = APIRouter()
|
|
|
logger = logging.getLogger("app.api.v1.earnings")
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
"/surprise/{symbol}",
|
|
|
response_model=EarningsSurpriseResponse,
|
|
|
summary="Get earnings surprise history",
|
|
|
description=(
|
|
|
"Quarterly EPS surprise: reported vs analyst consensus estimate.\n\n"
|
|
|
"**데이터 소스**: yfinance-plus (`Ticker.earnings_dates`). API 키 불필요.\n"
|
|
|
"**커버리지**: ~25분기 (6년+). 첫 조회 시 자동 인덱싱.\n\n"
|
|
|
"**surprise** = reported_eps - estimated_eps.\n"
|
|
|
"**surprise_percentage** = (surprise / estimated) × 100.\n"
|
|
|
"**streak**: 연속 beat (양수) 또는 miss (음수) 횟수."
|
|
|
),
|
|
|
)
|
|
|
@with_cache(namespace="earnings:surprise", ttl=None, key_params=["symbol", "quarters"])
|
|
|
async def get_earnings_surprise(
|
|
|
symbol: str,
|
|
|
response: Response,
|
|
|
quarters: int = Query(8, ge=1, le=40, description="Number of recent quarters (max ~25 available)"),
|
|
|
force_refresh: bool = Query(False, description="Bypass cache and re-fetch from yfinance"),
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
):
|
|
|
svc = EarningsService()
|
|
|
|
|
|
if force_refresh:
|
|
|
try:
|
|
|
await svc.index_earnings(db, symbol, force_refresh=True)
|
|
|
except ValueError as e:
|
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
|
|
|
|
try:
|
|
|
rows, stats = await svc.get_earnings_surprise(db, ticker=symbol, quarters=quarters)
|
|
|
except ValueError as e:
|
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
|
except Exception as e:
|
|
|
logger.error(f"Earnings surprise error for {symbol}: {e}")
|
|
|
raise HTTPException(status_code=502, detail=f"Failed to fetch earnings data: {e}")
|
|
|
|
|
|
entries = [EarningsSurpriseEntry.from_orm_obj(r) for r in rows]
|
|
|
|
|
|
return EarningsSurpriseResponse(
|
|
|
symbol=symbol.upper(),
|
|
|
quarters=entries,
|
|
|
streak=stats["streak"],
|
|
|
avg_surprise_pct=stats["avg_surprise_pct"],
|
|
|
metadata={
|
|
|
"data_source": "SEC_XBRL",
|
|
|
"quarters_requested": quarters,
|
|
|
"quarters_returned": len(entries),
|
|
|
"note": "surprise = current_eps - previous_quarter_eps (QoQ change)",
|
|
|
},
|
|
|
)
|