feat(fundamentals): 시가총액·PER·PBR·EPS·BPS·배당 — pykrx 스냅샷 + SymbolSidebar
backend:
- fetch/fundamentals.py: get_fundamentals(code) — pykrx 의 get_market_cap +
get_market_fundamental 을 같은 영업일로 정렬해서 호출. 토/일/공휴일이면 KRX 가 빈
응답을 줘서 최대 7일까지 역순 backoff. (code, today) 인메모리 daily 캐시로 중복 호출 방지.
- api/fundamentals.py: GET /api/fundamentals/{code} — 없는 코드 404, KRX 가 데이터를
안 주는 케이스는 status="no_data" + null 들. 200 응답으로 단순화 (호출자 분기 편의).
- main.py: fundamentals_router 등록.
frontend:
- lib/api.ts: FundamentalsResponse 타입 + api.fundamentals(code).
- components/SymbolSidebar.tsx: 1Y 차트와 별도로 fundamentals 호출. ok 일 때만
'기업 지표' 섹션 노출 — 시가총액(조/억) / 상장주식수 / PER / PBR / EPS / BPS /
배당수익률(%) / 주당배당금. PER·PBR 음수면 '적자' 표기 (이익 음수 → 멀티플 무의미).
- 기존 시세/52주 게이지 코멘트에 fundamentals 추가됨을 반영.
pykrx 종목당 호출 1~2초 → 첫 페이지 로딩이 살짝 느려질 수 있으나 daily 캐시로
재방문은 즉시. 펀더멘털 실패는 silent — 시세 표시는 영향 없음.
This commit is contained in:
63
backend/app/api/fundamentals.py
Normal file
63
backend/app/api/fundamentals.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""기업 펀더멘털 (시총/PER/PBR/EPS/BPS/배당) API.
|
||||
|
||||
pykrx 한 번 호출 = 종목당 ~1~2초. 인메모리 daily 캐시(get_fundamentals) 가 처리.
|
||||
SkippedMissingKey 같은 거 없음 — pykrx 는 무인증 KRX 스크래핑.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.db.connection import get_engine
|
||||
from app.fetch.fundamentals import get_fundamentals
|
||||
|
||||
router = APIRouter(prefix="/api/fundamentals", tags=["fundamentals"])
|
||||
|
||||
|
||||
@router.get("/{code}")
|
||||
def code_fundamentals(code: str) -> dict:
|
||||
"""code 의 최신 영업일 fundamentals 스냅샷."""
|
||||
eng = get_engine()
|
||||
with eng.connect() as conn:
|
||||
sym = conn.execute(
|
||||
text("SELECT code, name, market FROM symbols WHERE code = :c"),
|
||||
{"c": code},
|
||||
).first()
|
||||
if not sym:
|
||||
raise HTTPException(status_code=404, detail=f"unknown code: {code}")
|
||||
|
||||
f = get_fundamentals(code)
|
||||
if f is None:
|
||||
return {
|
||||
"code": sym[0],
|
||||
"name": sym[1],
|
||||
"market": sym[2],
|
||||
"status": "no_data",
|
||||
"snapshot_date": None,
|
||||
"market_cap": None,
|
||||
"shares_outstanding": None,
|
||||
"per": None,
|
||||
"pbr": None,
|
||||
"eps": None,
|
||||
"bps": None,
|
||||
"div": None,
|
||||
"dvd_yield": None,
|
||||
}
|
||||
|
||||
return {
|
||||
"code": sym[0],
|
||||
"name": sym[1],
|
||||
"market": sym[2],
|
||||
"status": "ok",
|
||||
"snapshot_date": f.snapshot_date.isoformat(),
|
||||
"market_cap": f.market_cap,
|
||||
"shares_outstanding": f.shares_outstanding,
|
||||
"per": f.per,
|
||||
"pbr": f.pbr,
|
||||
"eps": f.eps,
|
||||
"bps": f.bps,
|
||||
"div": f.div,
|
||||
"dvd_yield": f.dvd_yield,
|
||||
}
|
||||
Reference in New Issue
Block a user