Files
trading/bareunaapi/stock_loader.py
T
kyu 97cf5aecb0
trading/pipeline/head This commit looks good
chore: 누적 미커밋 작업분 일괄 커밋
이번 세션 외 그간 쌓인 변경 일괄 저장:
- bareunaapi: finance_dict 금융용어 / stock_loader 종목 로더 보강
- kis-api: 키움 토큰·수집 로직
- us-market / dart-collector: 수집 보강
- docker-compose: GEMINI_API_KEY 등 환경변수 추가
- score-engine/news-collector requirements, CLAUDE.md
- 신규: PROJECT.md, news-collector/sentiment_rules.py

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 01:24:16 +09:00

309 lines
13 KiB
Python

"""
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",
}
# 약칭/별칭 — 시장에서 통용되는 줄임말·구명·영문표기
# 주의: 2글자 이하 약칭은 일반어 충돌 위험 (예: "삼전" → "삼전자" 같은 일반어 거의 없어 OK,
# 하지만 "엔솔" 같은 건 위험 → "LG엔솔"처럼 회사명 포함 별칭만 유지)
ALIASES = {
# ─── 시총 상위 한국 종목 ───
# 반도체
"삼전": "005930", "삼성전자": "005930",
"하닉": "000660", "SK하닉": "000660", "SK하이닉스": "000660", "에스케이하이닉스": "000660",
"한미반도": "042700", "한미반": "042700",
"디비하이텍": "000990", "DB하이텍": "000990",
"리노공업": "058470",
"이수페타시스": "007660",
"주성엔지니어링": "036930", "주성": "036930",
# 2차전지/배터리
"LG엔솔": "373220", "엘지엔솔": "373220", "LG에너지": "373220",
"삼성SDI": "006400", "삼성에스디아이": "006400",
"SK이노": "096770", "에스케이이노베이션": "096770",
"에코프로비엠": "247540", "에코비엠": "247540",
"에코프로": "086520",
"포스코퓨처엠": "003670", "퓨처엠": "003670", "포퓨엠": "003670",
"엘앤에프": "066970", "L&F": "066970",
"엔켐": "348370",
# 자동차/모빌리티
"현차": "005380", "현대차": "005380", "현대자동차": "005380",
"기아차": "000270", "기아자동차": "000270", "기아": "000270",
"현모비스": "012330", "모비스": "012330", "현대모비스": "012330",
"현대글로비스": "086280", "글로비스": "086280",
"현대위아": "011210",
# 바이오/제약
"삼바": "207940", "삼성바이오": "207940", "삼성바이오로직스": "207940",
"셀트리": "068270", "셀트리온": "068270",
"셀트리온헬스케어": "091990",
"한미약": "128940", "한미약품": "128940",
"유한양": "000100", "유한양행": "000100",
"녹십자": "006280",
"알테오젠": "196170",
"에이비엘": "298380", "에이비엘바이오": "298380",
"리가켐": "141080", "리가켐바이오": "141080",
# 인터넷/IT
"네이버": "035420", "NAVER": "035420",
"카카오": "035720",
"카뱅": "323410", "카카오뱅크": "323410",
"카페이": "377300", "카카오페이": "377300",
"카게임": "293490", "카카오게임즈": "293490",
# 게임/엔터
"엔씨": "036570", "엔씨소프트": "036570",
"넷마블": "251270",
"크래프톤": "259960", "크라프톤": "259960",
"위메이드": "112040",
"펄어비스": "263750",
"하이브": "352820", "BTS하이브": "352820",
"SM": "041510", "에스엠": "041510",
"JYP": "035900", "JYPEnt": "035900",
"와이지": "122870", "YG": "122870",
# 방산/조선
"한에솔": "012450", "한화에어": "012450", "한화에어로": "012450", "한화에어로스페이스": "012450",
"LIG넥": "079550", "LIG넥스원": "079550",
"현대로템": "064350", "로템": "064350",
"HD현중": "329180", "HD현대중공업": "329180", "현중": "329180",
"한국조선해양": "009540", "HD한국조선": "009540", "HD현대": "267250",
"삼성중공업": "010140", "삼중": "010140",
"한화오션": "042660", "대우조선": "042660",
"한화시스템": "272210",
# 화학/소재
"LG화학": "051910",
"롯데케미칼": "011170", "롯데케": "011170",
"한화솔루션": "009830", "한솔": "009830",
"효성첨단": "298050", "효성첨단소재": "298050",
"코오롱인": "120110", "코오롱인더": "120110",
"금호석화": "011780", "금호석유": "011780",
"OCI": "010060",
# 철강/소재
"포홀": "005490", "POSCO": "005490", "포스코홀딩스": "005490",
"POSCO홀딩스": "005490", "포스코": "005490",
"현대제철": "004020", "현제철": "004020",
"고려아연": "010130",
# 금융/지주
"KB금융": "105560", "케이비금융": "105560",
"신한지주": "055550", "신한금융": "055550",
"하나금융": "086790", "하나금융지주": "086790",
"우리금융": "316140", "우리금융지주": "316140",
"기업은행": "024110", "IBK기업은행": "024110",
"메리츠금융": "138040", "메리츠금융지주": "138040",
"삼성생명": "032830", "삼생": "032830",
"삼성화재": "000810", "삼화": "000810",
"DB손보": "005830", "DB손해보험": "005830",
"현대해상": "001450",
"한투지": "071050", "한국금융지주": "071050",
"키움증권": "039490", "키움": "039490",
"미래에셋": "006800", "미래에셋증권": "006800",
# 통신
"SK텔": "017670", "SKT": "017670", "SK텔레콤": "017670",
"KT": "030200",
"LGU+": "032640", "LG유플러스": "032640", "엘지유플": "032640",
# 유통/소비재
"이마트": "139480",
"롯데쇼핑": "023530",
"신세계": "004170",
"현대백화점": "069960",
"GS리테일": "007070",
"BGF리테일": "282330", "씨유": "282330", "CU": "282330",
"오리온": "271560",
"농심": "004370",
"삼양식품": "003230", "삼양": "003230",
"CJ제일제당": "097950", "CJ제일": "097950",
"오뚜기": "007310",
"하이트진로": "000080",
"롯데칠성": "005300",
"코카콜라음료": "005440",
"아모레퍼시픽": "090430", "아모레": "090430",
"LG생건": "051900", "LG생활건강": "051900",
"코스맥스": "192820",
"한국콜마": "161890",
# 건설
"삼성물산": "028260",
"현대건설": "000720", "현건": "000720",
"GS건설": "006360",
"대우건설": "047040",
"DL이앤씨": "375500", "디엘이앤씨": "375500",
"HDC현대산업개발": "294870", "HDC현산": "294870",
# 에너지/유틸리티
"한전": "015760", "한국전력": "015760",
"한국가스공사": "036460", "한가공": "036460",
"두산에너빌리티": "034020", "두산에너": "034020", "두에빌": "034020",
"두산퓨얼셀": "336260", "두산퓨얼": "336260",
"두산밥캣": "241560",
"두산": "000150",
"S-Oil": "010950", "S오일": "010950", "에쓰오일": "010950",
"SK이노베이션": "096770",
"GS": "078930",
"한화": "000880",
# 항공/운송
"대한항공": "003490", "대항": "003490",
"아시아나": "020560", "아시아나항공": "020560",
"제주항공": "089590",
"진에어": "272450",
"티웨이항공": "091810",
"HMM": "011200",
"팬오션": "028670",
"현대글로비스": "086280",
# 우주/위성
"한화에어로스페이스": "012450",
"쎄트렉아이": "099320", "쎄트렉": "099320",
"인텔리안테크": "189300", "인텔리안": "189300",
"AP위성": "211270",
# AI/디스플레이
"LG디스플": "034220", "LG디스플레이": "034220", "엘지디스플레이": "034220",
"더존비즈온": "012510", "더존": "012510",
"삼성전기": "009150", "삼전기": "009150",
"LG이노텍": "011070",
# 보안/SI
"안랩": "053800",
"케이아이엔엑스": "093320", "KINX": "093320",
}
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())
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))