6d3b0bacc0
- 19개 마이크로서비스 (news-collector, score-engine, ta-engine, dart-collector, aux-signal, us-market, graph-engine, telegram-bot, dashboard-api, kis-api 등) - 가치투자 스코어링 + 10공식 앙상블 보팅 (매직포뮬러/F-Score/Altman/PEG/ 모멘텀/Beneish/GP-A/G-Score/Amihud/BAB) - 뉴스 수집→형태소→임베딩→중복제거→AI분석 파이프라인 - 기술적분석 + GAT 그래프신경망 + 미증시 동조 시그널 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
163 lines
6.2 KiB
Python
163 lines
6.2 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",
|
|
}
|
|
|
|
# 약칭/별칭
|
|
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))
|