"""섹터 매핑 캐시 수집 배치 (CLAUDE.md §8.5 항목 8) 네이버 업종 API에서 전 종목 업종 → 정본 섹터 라벨을 수집해 `~/.cache/auto-stock/sector_map.json` 에 적재한다. 적재된 캐시는 `sector_mapper.get_sector()`(동기·네트워크 없음)를 통해 correlation_filter §5.1 섹터캡 판정에 쓰인다. 사용법: python collect_sector_map.py # 수집 + 캐시 기록 + 커버리지 리포트 python collect_sector_map.py --dry-run # 수집만 (캐시 파일 미기록) python collect_sector_map.py --coverage-only # 기존 캐시로 커버리지만 측정 (네트워크 최소) Cron 권장 (상장/업종 변경 반영 — 하루 1회로 충분): 30 8 * * 1-5 cd /path/to/auto-stock/backend && python collect_sector_map.py 주의: DB 를 쓰지 않는다(파일 캐시 전용) → db_worker 경유 규칙 대상 아님. """ import argparse import asyncio import logging import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from app.core.json_helpers import _dumps # noqa: E402 from app.services import sector_mapper # noqa: E402 logger = logging.getLogger(__name__) async def _universe_codes(max_pages: int = 10) -> list: """커버리지 측정용 유니버스(네이버 시총 상위 KOSPI+KOSDAQ) 종목코드.""" try: from app.services.naver_finance import get_naver_stock_list stocks = await get_naver_stock_list(max_pages_per_market=max_pages) return [code for code, _name in stocks] except Exception as exc: logger.warning("유니버스 조회 실패 (커버리지 측정 생략): %s", exc) return [] async def main() -> int: parser = argparse.ArgumentParser(description="섹터 매핑 캐시 수집") parser.add_argument("--dry-run", action="store_true", help="캐시 파일에 기록하지 않음") parser.add_argument("--coverage-only", action="store_true", help="수집 없이 커버리지만 측정") parser.add_argument("--no-coverage", action="store_true", help="커버리지 측정 생략(유니버스 조회 안 함)") parser.add_argument("--universe-pages", type=int, default=10, help="시장별 유니버스 페이지 수 (기본 10=~500종목/시장)") args = parser.parse_args() if not args.coverage_only: result = await sector_mapper.refresh_sector_map(write_cache=not args.dry_run) print(_dumps({"refresh": result})) if not result.get("ok"): logger.error("섹터 맵 수집 실패 — 기존 캐시/정적 표 폴백 유지") return 1 if args.no_coverage: return 0 universe = await _universe_codes(args.universe_pages) coverage = sector_mapper.get_coverage(universe or None) print(_dumps({"coverage": coverage})) pct = coverage.get("universe_coverage_pct") if pct is not None: logger.info( "섹터 커버리지 %.2f%% (%s/%s) — 80%% 이상이면 SECTOR_CAP_UNMAPPED_ENABLED 검토 가능", pct, coverage.get("universe_covered"), coverage.get("universe_size"), ) return 0 if __name__ == "__main__": logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) sys.exit(asyncio.run(main()))