Files

163 lines
6.2 KiB
Python
Raw Permalink Normal View History

"""
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})[^>]*>([^<]+)</a>", 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))