""" KRX 전체 종목 동적 로딩 - 서버 시작 시 1회 로딩 - 매일 자정 자동 갱신 - 실패 시 하드코딩 폴백 """ import asyncio import json import io import time from datetime import datetime import httpx import structlog logger = structlog.get_logger() # 폴백 종목 (네트워크 실패 시 사용) FALLBACK_STOCKS = { "코스피": "KOSPI", "코스닥": "KOSDAQ", "코스피200": "KOSPI200", "삼성전자": "005930", "SK하이닉스": "000660", "LG에너지솔루션": "373220", "현대차": "005380", "기아": "000270", "셀트리온": "068270", "카카오": "035720", "네이버": "035420", "NAVER": "035420", "삼성바이오로직스": "207940", "KB금융": "105560", "POSCO홀딩스": "005490", "신한지주": "055550", "LG화학": "051910", "삼성SDI": "006400", "현대모비스": "012330", "하나금융지주": "086790", "SK텔레콤": "017670", "KT": "030200", "LG전자": "066570", "한화에어로스페이스": "012450", "삼성물산": "028260", "HD현대중공업": "329180", "한국전력": "015760", "HMM": "011200", "대한항공": "003490", "카카오뱅크": "323410", "크래프톤": "259960", "하이브": "352820", "에코프로": "086520", "알테오젠": "196170", "한미반도체": "042700", } # 약칭/별칭 ALIASES = { "삼전": "005930", "하닉": "000660", "현차": "005380", "카뱅": "323410", "삼바": "207940", "삼성바이오": "207940", "한에솔": "012450", "한화에어": "012450", "LG엔솔": "373220", "SK하닉": "000660", "포홀": "005490", "에프엔에프": "383220", "현대자동차": "005380", "네이버": "035420", } async def fetch_krx_stocks() -> dict[str, str]: """KRX에서 전체 상장 종목 가져오기""" stock_map = { "코스피": "KOSPI", "코스닥": "KOSDAQ", "코스피200": "KOSPI200", "KRX": "KRX", } headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "http://data.krx.co.kr/contents/MDC/MDI/mdiLoader/index.cmd", } async with httpx.AsyncClient(timeout=30, headers=headers) as client: for mkt_id, mkt_name in [("STK", "KOSPI"), ("KSQ", "KOSDAQ")]: try: payload = { "bld": "dbms/MDC/STAT/standard/MDCSTAT01901", "locale": "ko_KR", "mktId": mkt_id, "share": "1", "csvxls_is498": "false", } resp = await client.post( "http://data.krx.co.kr/comm/bldAttend/getJsonData.cmd", data=payload, ) if resp.status_code == 200: data = resp.json() items = data.get("OutBlock_1", []) for item in items: name = item.get("ISU_ABBRV", "").strip() code = item.get("ISU_SRT_CD", "").strip() if name and code and len(code) == 6: stock_map[name] = code logger.info("krx.loaded", market=mkt_name, count=len(items)) else: logger.warning("krx.http_error", market=mkt_name, status=resp.status_code) except Exception as e: logger.warning("krx.fetch_failed", market=mkt_name, error=str(e)) # KRX 실패 시 네이버 금융 폴백 if len(stock_map) < 100: logger.info("krx.fallback_naver", reason="KRX returned too few stocks") stock_map.update(await fetch_naver_stocks(client=None)) return stock_map async def fetch_naver_stocks() -> dict[str, str]: """네이버 금융에서 종목 가져오기 (KRX 폴백)""" stock_map = {} headers = {"User-Agent": "Mozilla/5.0"} import re async with httpx.AsyncClient(timeout=15, headers=headers) as client: for sosok in [0, 1]: # 0=KOSPI, 1=KOSDAQ for page in range(1, 45): try: url = f"https://finance.naver.com/sise/sise_market_sum.naver?sosok={sosok}&page={page}" resp = await client.get(url) text = resp.content.decode("euc-kr", errors="ignore") rows = re.findall( r"main\.naver\?code=(\d{6})[^>]*>([^<]+)", text ) if not rows: break for code, name in rows: name = name.strip() if name and code: stock_map[name] = code except Exception: break logger.info("naver.loaded", count=len(stock_map)) return stock_map async def load_all_stocks() -> dict[str, str]: """전체 종목 로딩 (KRX → 네이버 → 폴백)""" try: stock_map = await fetch_krx_stocks() if len(stock_map) > 100: stock_map.update(ALIASES) logger.info("stocks.loaded", total=len(stock_map), source="KRX") return stock_map except Exception as e: logger.warning("stocks.krx_failed", error=str(e)) try: stock_map = await fetch_naver_stocks() if len(stock_map) > 100: stock_map.update({ "코스피": "KOSPI", "코스닥": "KOSDAQ", "코스피200": "KOSPI200", "KRX": "KRX", }) stock_map.update(ALIASES) logger.info("stocks.loaded", total=len(stock_map), source="Naver") return stock_map except Exception as e: logger.warning("stocks.naver_failed", error=str(e)) # 최종 폴백 fallback = {**FALLBACK_STOCKS, **ALIASES} logger.warning("stocks.using_fallback", total=len(fallback)) return fallback async def auto_refresh_stocks(state_ref, interval_hours: int = 24): """백그라운드에서 종목 목록 자동 갱신""" while True: await asyncio.sleep(interval_hours * 3600) try: new_map = await load_all_stocks() if len(new_map) > 50: state_ref.stock_map = new_map logger.info("stocks.refreshed", total=len(new_map)) except Exception as e: logger.warning("stocks.refresh_failed", error=str(e))