You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
71 lines
2.6 KiB
Python
71 lines
2.6 KiB
Python
"""
|
|
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 data from SEC EDGAR XBRL.\n\n"
|
|
"**데이터 소스**: SEC EDGAR companyfacts API (무료, API 키 불필요).\n"
|
|
"**커버리지**: 2009년~ (대부분 종목). 첫 조회 시 자동 인덱싱.\n\n"
|
|
"**surprise**: 현재 분기 EPS - 이전 분기 EPS (QoQ 변화).\n"
|
|
"**estimated_eps**: 이전 분기 EPS (비교 기준).\n"
|
|
"**streak**: 연속 QoQ 증가 (양수) 또는 감소 (음수) 횟수."
|
|
),
|
|
)
|
|
@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=80, description="Number of recent quarters (max 80 = ~20 years)"),
|
|
force_refresh: bool = Query(False, description="Bypass cache and re-fetch from SEC EDGAR"),
|
|
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)",
|
|
},
|
|
)
|