From 1f0017a4ea8e1c6182bd417a8c9bc585c977c07a Mon Sep 17 00:00:00 2001 From: I Luk Kim Date: Mon, 20 Apr 2026 16:43:37 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20company=20metadata=20placeholder=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20=E2=80=94=20sector/industry=20=EC=8B=A4?= =?UTF-8?q?=EC=A0=9C=20=EA=B0=92=20=EB=B3=B4=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/api/v1/endpoints/financial.py | 9 +++++++++ app/services/company_metadata_service.py | 11 +++++++++-- app/services/financial_service.py | 16 ++++++++++------ docker-compose.yml | 2 +- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/app/api/v1/endpoints/financial.py b/app/api/v1/endpoints/financial.py index 43bfa37..662fbe8 100644 --- a/app/api/v1/endpoints/financial.py +++ b/app/api/v1/endpoints/financial.py @@ -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 diff --git a/app/services/company_metadata_service.py b/app/services/company_metadata_service.py index 0d3b6fe..5590ef4 100644 --- a/app/services/company_metadata_service.py +++ b/app/services/company_metadata_service.py @@ -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 diff --git a/app/services/financial_service.py b/app/services/financial_service.py index 79c0bc8..ec17219 100644 --- a/app/services/financial_service.py +++ b/app/services/financial_service.py @@ -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.""" diff --git a/docker-compose.yml b/docker-compose.yml index 25948e0..c3b6b20 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: