97cf5aecb0
trading/pipeline/head This commit looks good
이번 세션 외 그간 쌓인 변경 일괄 저장: - 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>
309 lines
13 KiB
Python
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))
|