fix: company metadata placeholder 제거 — sector/industry 실제 값 보장

- financial.py: 캐시 히트 시 company 블록을 CMS에서 항상 refresh
  (financial:data:* 캐시에 stale company 블록이 임베드된 문제 해결)
- financial_service.py: _is_placeholder() 확장 — exchange=null인
  Technology/Software 종목도 placeholder로 감지 (AVGO-류 미검출 해결)
- company_metadata_service.py: _sync_fetch() 개선 — yfinance 첫 번째
  호출은 auth 토큰 없이 partial 응답을 반환할 수 있으므로 sector가
  없는 EQUITY 종목에 대해 한 번 retry (auth 캐시 후 full data 획득)
- docker-compose.yml: 컨테이너 시작 시 yfinance_plus 로컬 버전을
  site-packages에 자동 복사 (docker restart 후 override 소실 방지)

검증: USAS/CPRX/HE/ACHR/AVGO 모두 실제 sector/industry/exchange 반환

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
I Luk Kim 4 months ago
parent b322d28bd9
commit 1f0017a4ea

@ -142,6 +142,15 @@ async def get_financial_data(
cached = await get_cached_response(cache_key)
if cached:
cached_body, etag = cached
# Always refresh company block from CMS — financial cache TTL is 24h
# but company metadata may have been enriched independently.
# CMS has its own company:meta:* Redis cache so this is cheap.
try:
from app.services import company_metadata_service as cms
fresh_meta = await cms.get_metadata(db, request.ticker.upper())
cached_body["company"] = fresh_meta
except Exception:
pass # keep stale block if CMS unavailable
response.headers["X-Cache"] = "HIT"
response.headers["Cache-Control"] = f"public, max-age={_FIN_TTL}"
response.headers["ETag"] = etag

@ -50,8 +50,15 @@ async def _fetch_yfinance_info(ticker: str) -> Optional[dict]:
def _sync_fetch():
try:
t = yf.Ticker(ticker)
return t.info
import yfinance as _base_yf # Always use base yfinance for auth reliability
info = _base_yf.Ticker(ticker).info
# yfinance's first HTTP call may lack a valid auth token and return
# a partial response (sector/industry = None). Retry once if an
# equity ticker is missing sector — the second call uses the auth
# token that was cached by the first call.
if info and not info.get("sector") and info.get("quoteType") == "EQUITY":
info = _base_yf.Ticker(ticker).info
return info
except Exception as e:
logger.warning("yfinance .info failed for %s: %s", ticker, e)
return None

@ -129,12 +129,16 @@ class FinancialService:
@staticmethod
def _is_placeholder(company: Company) -> bool:
"""True when the row was created by the old hardcoded-defaults path."""
return bool(
company.sector == "Technology"
and company.industry == "Software"
and company.name
and company.name.endswith(" Corporation")
)
if not (company.sector == "Technology" and company.industry == "Software"):
return False
# Explicit placeholder: synthetic name
if company.name and company.name.endswith(" Corporation"):
return True
# Implicit placeholder: Technology/Software assigned but no exchange means
# yfinance never actually enriched this row (old defaults path).
if not getattr(company, "exchange", None):
return True
return False
async def _get_or_create_company(self, db: AsyncSession, ticker: str) -> Company:
"""Get or create company record, enriching via CompanyMetadataService."""

@ -68,7 +68,7 @@ services:
mem_limit: 3g
memswap_limit: 3g
restart: unless-stopped
command: ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "18000", "--limit-concurrency", "25"]
command: ["sh", "-c", "cp /app/yfinance_plus/yfinance_plus.py /usr/local/lib/python3.11/site-packages/yfinance_plus.py && python -m uvicorn app.main:app --host 0.0.0.0 --port 18000 --limit-concurrency 25"]
# Frontend Application
frontend:

Loading…
Cancel
Save