feat(seed): KRX live 실패 시 정적 주요종목 fallback (156개)
배경: 토요일/봇차단 등으로 KRX 가 LOGOUT/JSONDecodeError 를 주면 pykrx 가 빈 리스트만 돌려줘서 SEED 10 종목 외엔 검색이 안 되는 회귀가 있었다 (기아/네이버/엔비디아 같은 비-seed 검색 실패). 해결: 시총/거래대금 상위 + 친숙도 기준으로 KOSPI 104 / KOSDAQ 52 = 156 개를 정적 스냅샷 (app/seed/krx_static.py) 으로 묶었다. seed_symbols() 가 KRX 라이브 fetch 결과가 0건이면 자동으로 이 스냅샷에서 upsert. KRX 가 정상 돌아오면 다음 호출이 라이브 데이터로 자연 덮어쓰여진다. 응답에 static_fallback_used 필드 노출. 수록 항목은 KRX 공시 종목코드 + 현재 등록명 (공개 사실). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,7 @@ def reseed_symbols() -> dict:
|
|||||||
"updated": report.updated,
|
"updated": report.updated,
|
||||||
"seed_marked": report.seed_marked,
|
"seed_marked": report.seed_marked,
|
||||||
"markets": report.markets,
|
"markets": report.markets,
|
||||||
|
"static_fallback_used": report.static_fallback_used,
|
||||||
}
|
}
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
# seed_symbols 내부에서 다 잡지만, 만에 하나 외부로 새는 예외 (logger 포매터
|
# seed_symbols 내부에서 다 잡지만, 만에 하나 외부로 새는 예외 (logger 포매터
|
||||||
@@ -68,5 +69,6 @@ def reseed_symbols() -> dict:
|
|||||||
"updated": 0,
|
"updated": 0,
|
||||||
"seed_marked": 0,
|
"seed_marked": 0,
|
||||||
"markets": {},
|
"markets": {},
|
||||||
|
"static_fallback_used": False,
|
||||||
"error": repr(e)[:300],
|
"error": repr(e)[:300],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from dataclasses import dataclass
|
|||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
from app.db.connection import get_engine
|
from app.db.connection import get_engine
|
||||||
|
from app.seed.krx_static import KRX_STATIC_MAJORS
|
||||||
from app.seed.seed_tickers import SEED_CODES, SEED_TICKERS
|
from app.seed.seed_tickers import SEED_CODES, SEED_TICKERS
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -22,6 +23,7 @@ class SeedReport:
|
|||||||
updated: int
|
updated: int
|
||||||
seed_marked: int
|
seed_marked: int
|
||||||
markets: dict[str, int]
|
markets: dict[str, int]
|
||||||
|
static_fallback_used: bool = False
|
||||||
|
|
||||||
|
|
||||||
def _fetch_market_listing(market: str) -> list[tuple[str, str]]:
|
def _fetch_market_listing(market: str) -> list[tuple[str, str]]:
|
||||||
@@ -99,6 +101,21 @@ def seed_symbols() -> SeedReport:
|
|||||||
logger.error("seed_symbols: KRX %s fetch failed — skip market: %s", market, repr(e)[:300])
|
logger.error("seed_symbols: KRX %s fetch failed — skip market: %s", market, repr(e)[:300])
|
||||||
market_counts[market] = 0
|
market_counts[market] = 0
|
||||||
|
|
||||||
|
# 2b) KRX 가 0건만 줬으면 정적 스냅샷 (krx_static.KRX_STATIC_MAJORS) 으로 fallback.
|
||||||
|
# KRX 가 LOGOUT/JSONDecode 로 막혀도 주요 종목 검색은 동작하게 만든다.
|
||||||
|
# KRX 가 정상으로 돌아오면 다음 호출에서 all_rows 가 채워져 자동으로 덮어쓰여진다.
|
||||||
|
static_fallback_used = False
|
||||||
|
if not all_rows:
|
||||||
|
logger.warning(
|
||||||
|
"seed_symbols: KRX live fetch empty — fallback to static snapshot (%d majors)",
|
||||||
|
len(KRX_STATIC_MAJORS),
|
||||||
|
)
|
||||||
|
all_rows = list(KRX_STATIC_MAJORS)
|
||||||
|
static_fallback_used = True
|
||||||
|
# market_counts 도 fallback 으로 채워 응답에 반영
|
||||||
|
for _c, _n, m in KRX_STATIC_MAJORS:
|
||||||
|
market_counts[m] = market_counts.get(m, 0) + 1
|
||||||
|
|
||||||
inserted = updated = 0
|
inserted = updated = 0
|
||||||
if all_rows:
|
if all_rows:
|
||||||
engine = get_engine()
|
engine = get_engine()
|
||||||
@@ -129,7 +146,13 @@ def seed_symbols() -> SeedReport:
|
|||||||
logger.error("seed_symbols: KRX bulk upsert failed (transaction rolled back): %s", repr(e)[:300])
|
logger.error("seed_symbols: KRX bulk upsert failed (transaction rolled back): %s", repr(e)[:300])
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"seed_symbols done: inserted=%d updated=%d seed_marked=%d markets=%s",
|
"seed_symbols done: inserted=%d updated=%d seed_marked=%d markets=%s static_fallback=%s",
|
||||||
inserted, updated, seed_marked, market_counts,
|
inserted, updated, seed_marked, market_counts, static_fallback_used,
|
||||||
|
)
|
||||||
|
return SeedReport(
|
||||||
|
inserted=inserted,
|
||||||
|
updated=updated,
|
||||||
|
seed_marked=seed_marked,
|
||||||
|
markets=market_counts,
|
||||||
|
static_fallback_used=static_fallback_used,
|
||||||
)
|
)
|
||||||
return SeedReport(inserted=inserted, updated=updated, seed_marked=seed_marked, markets=market_counts)
|
|
||||||
|
|||||||
178
backend/app/seed/krx_static.py
Normal file
178
backend/app/seed/krx_static.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
"""KRX 주요 종목 정적 스냅샷.
|
||||||
|
|
||||||
|
용도: pykrx → KRX 실시간 호출이 LOGOUT/JSONDecodeError 로 실패할 때
|
||||||
|
symbols 테이블 검색 universe 를 최소한 보장하기 위한 fallback.
|
||||||
|
|
||||||
|
수록 범위:
|
||||||
|
- KOSPI/KOSDAQ 시가총액 상위 + 일반 사용자가 검색할 법한 친숙한 이름 위주.
|
||||||
|
- 종목코드(KRX 공시)와 종목명(현재 등록명)은 공개된 사실 정보.
|
||||||
|
- 전 종목은 KRX 실시간이 복구되면 seed_symbols() 가 자동 덮어쓴다.
|
||||||
|
|
||||||
|
정확성 주의:
|
||||||
|
- 회사 합병/분할/사명변경/상장폐지가 잦아 일부 항목은 시간 지나면 stale.
|
||||||
|
- KRX 호출이 정상화되면 자동 upsert 로 덮어쓰므로 stale 항목은 자연 보정.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# (code, name, market)
|
||||||
|
KRX_STATIC_MAJORS: list[tuple[str, str, str]] = [
|
||||||
|
# ---------------- KOSPI (시총 상위 + 익숙한 종목) ----------------
|
||||||
|
("005930", "삼성전자", "KOSPI"),
|
||||||
|
("005935", "삼성전자우", "KOSPI"),
|
||||||
|
("000660", "SK하이닉스", "KOSPI"),
|
||||||
|
("373220", "LG에너지솔루션", "KOSPI"),
|
||||||
|
("207940", "삼성바이오로직스", "KOSPI"),
|
||||||
|
("005380", "현대차", "KOSPI"),
|
||||||
|
("000270", "기아", "KOSPI"),
|
||||||
|
("035420", "NAVER", "KOSPI"),
|
||||||
|
("068270", "셀트리온", "KOSPI"),
|
||||||
|
("005490", "POSCO홀딩스", "KOSPI"),
|
||||||
|
("105560", "KB금융", "KOSPI"),
|
||||||
|
("035720", "카카오", "KOSPI"),
|
||||||
|
("028260", "삼성물산", "KOSPI"),
|
||||||
|
("055550", "신한지주", "KOSPI"),
|
||||||
|
("012330", "현대모비스", "KOSPI"),
|
||||||
|
("086790", "하나금융지주", "KOSPI"),
|
||||||
|
("051910", "LG화학", "KOSPI"),
|
||||||
|
("006400", "삼성SDI", "KOSPI"),
|
||||||
|
("003550", "LG", "KOSPI"),
|
||||||
|
("015760", "한국전력공사", "KOSPI"),
|
||||||
|
("032830", "삼성생명", "KOSPI"),
|
||||||
|
("003670", "포스코퓨처엠", "KOSPI"),
|
||||||
|
("017670", "SK텔레콤", "KOSPI"),
|
||||||
|
("009150", "삼성전기", "KOSPI"),
|
||||||
|
("011200", "HMM", "KOSPI"),
|
||||||
|
("138040", "메리츠금융지주", "KOSPI"),
|
||||||
|
("316140", "우리금융지주", "KOSPI"),
|
||||||
|
("018260", "삼성에스디에스", "KOSPI"),
|
||||||
|
("010130", "고려아연", "KOSPI"),
|
||||||
|
("034730", "SK", "KOSPI"),
|
||||||
|
("030200", "KT", "KOSPI"),
|
||||||
|
("066570", "LG전자", "KOSPI"),
|
||||||
|
("011070", "LG이노텍", "KOSPI"),
|
||||||
|
("010950", "S-Oil", "KOSPI"),
|
||||||
|
("047810", "한국항공우주", "KOSPI"),
|
||||||
|
("024110", "기업은행", "KOSPI"),
|
||||||
|
("138930", "BNK금융지주", "KOSPI"),
|
||||||
|
("010140", "삼성중공업", "KOSPI"),
|
||||||
|
("251270", "넷마블", "KOSPI"),
|
||||||
|
("028050", "삼성E&A", "KOSPI"),
|
||||||
|
("042660", "한화오션", "KOSPI"),
|
||||||
|
("086280", "현대글로비스", "KOSPI"),
|
||||||
|
("064350", "현대로템", "KOSPI"),
|
||||||
|
("042700", "한미반도체", "KOSPI"),
|
||||||
|
("267260", "HD현대일렉트릭", "KOSPI"),
|
||||||
|
("329180", "HD현대중공업", "KOSPI"),
|
||||||
|
("010620", "현대미포조선", "KOSPI"),
|
||||||
|
("161390", "한국타이어앤테크놀로지", "KOSPI"),
|
||||||
|
("097950", "CJ제일제당", "KOSPI"),
|
||||||
|
("271560", "오리온", "KOSPI"),
|
||||||
|
("175330", "JB금융지주", "KOSPI"),
|
||||||
|
("139480", "이마트", "KOSPI"),
|
||||||
|
("023530", "롯데쇼핑", "KOSPI"),
|
||||||
|
("088350", "한화생명", "KOSPI"),
|
||||||
|
("088980", "맥쿼리인프라", "KOSPI"),
|
||||||
|
("004020", "현대제철", "KOSPI"),
|
||||||
|
("047050", "포스코인터내셔널", "KOSPI"),
|
||||||
|
("069960", "현대백화점", "KOSPI"),
|
||||||
|
("003490", "대한항공", "KOSPI"),
|
||||||
|
("035250", "강원랜드", "KOSPI"),
|
||||||
|
("071050", "한국금융지주", "KOSPI"),
|
||||||
|
("000810", "삼성화재", "KOSPI"),
|
||||||
|
("011170", "롯데케미칼", "KOSPI"),
|
||||||
|
("000720", "현대건설", "KOSPI"),
|
||||||
|
("000150", "두산", "KOSPI"),
|
||||||
|
("034020", "두산에너빌리티", "KOSPI"),
|
||||||
|
("282330", "BGF리테일", "KOSPI"),
|
||||||
|
("007070", "GS리테일", "KOSPI"),
|
||||||
|
("078930", "GS", "KOSPI"),
|
||||||
|
("011780", "금호석유", "KOSPI"),
|
||||||
|
("002790", "아모레G", "KOSPI"),
|
||||||
|
("090430", "아모레퍼시픽", "KOSPI"),
|
||||||
|
("001040", "CJ", "KOSPI"),
|
||||||
|
("005180", "빙그레", "KOSPI"),
|
||||||
|
("049770", "동원F&B", "KOSPI"),
|
||||||
|
("005300", "롯데칠성", "KOSPI"),
|
||||||
|
("000080", "하이트진로", "KOSPI"),
|
||||||
|
("004990", "롯데지주", "KOSPI"),
|
||||||
|
("000100", "유한양행", "KOSPI"),
|
||||||
|
("009540", "HD한국조선해양", "KOSPI"),
|
||||||
|
("010060", "OCI홀딩스", "KOSPI"),
|
||||||
|
("011790", "SKC", "KOSPI"),
|
||||||
|
("004170", "신세계", "KOSPI"),
|
||||||
|
("008770", "호텔신라", "KOSPI"),
|
||||||
|
("002380", "KCC", "KOSPI"),
|
||||||
|
("014820", "동원시스템즈", "KOSPI"),
|
||||||
|
("005830", "DB손해보험", "KOSPI"),
|
||||||
|
("089860", "롯데렌탈", "KOSPI"),
|
||||||
|
("009830", "한화솔루션", "KOSPI"),
|
||||||
|
("006650", "대한유화", "KOSPI"),
|
||||||
|
("000240", "한국앤컴퍼니", "KOSPI"),
|
||||||
|
("026960", "동서", "KOSPI"),
|
||||||
|
("001450", "현대해상", "KOSPI"),
|
||||||
|
("003410", "쌍용C&E", "KOSPI"),
|
||||||
|
("003090", "대웅", "KOSPI"),
|
||||||
|
("069620", "대웅제약", "KOSPI"),
|
||||||
|
("128940", "한미약품", "KOSPI"),
|
||||||
|
("004000", "롯데정밀화학", "KOSPI"),
|
||||||
|
("000120", "CJ대한통운", "KOSPI"),
|
||||||
|
("005880", "대한해운", "KOSPI"),
|
||||||
|
("001230", "동국홀딩스", "KOSPI"),
|
||||||
|
("000990", "DB하이텍", "KOSPI"),
|
||||||
|
("036570", "엔씨소프트", "KOSPI"),
|
||||||
|
("267250", "HD현대", "KOSPI"),
|
||||||
|
|
||||||
|
# ---------------- KOSDAQ (시총/거래대금/이름 인지도) ----------------
|
||||||
|
("247540", "에코프로비엠", "KOSDAQ"),
|
||||||
|
("086520", "에코프로", "KOSDAQ"),
|
||||||
|
("035760", "CJ ENM", "KOSDAQ"),
|
||||||
|
("058470", "리노공업", "KOSDAQ"),
|
||||||
|
("196170", "알테오젠", "KOSDAQ"),
|
||||||
|
("091990", "셀트리온헬스케어", "KOSDAQ"),
|
||||||
|
("028300", "HLB", "KOSDAQ"),
|
||||||
|
("357780", "솔브레인", "KOSDAQ"),
|
||||||
|
("263750", "펄어비스", "KOSDAQ"),
|
||||||
|
("293490", "카카오게임즈", "KOSDAQ"),
|
||||||
|
("145020", "휴젤", "KOSDAQ"),
|
||||||
|
("277810", "레인보우로보틱스", "KOSDAQ"),
|
||||||
|
("240810", "원익IPS", "KOSDAQ"),
|
||||||
|
("357180", "LX세미콘", "KOSDAQ"),
|
||||||
|
("376300", "디어유", "KOSDAQ"),
|
||||||
|
("095610", "테스", "KOSDAQ"),
|
||||||
|
("067160", "아프리카TV", "KOSDAQ"),
|
||||||
|
("067310", "하나마이크론", "KOSDAQ"),
|
||||||
|
("388790", "케이엠더블유", "KOSDAQ"),
|
||||||
|
("089030", "테크윙", "KOSDAQ"),
|
||||||
|
("086900", "메디톡스", "KOSDAQ"),
|
||||||
|
("035900", "JYP Ent.", "KOSDAQ"),
|
||||||
|
("041510", "에스엠", "KOSDAQ"),
|
||||||
|
("122870", "와이지엔터테인먼트", "KOSDAQ"),
|
||||||
|
("352820", "하이브", "KOSDAQ"),
|
||||||
|
("039030", "이오테크닉스", "KOSDAQ"),
|
||||||
|
("066970", "엘앤에프", "KOSDAQ"),
|
||||||
|
("112040", "위메이드", "KOSDAQ"),
|
||||||
|
("194700", "네오위즈", "KOSDAQ"),
|
||||||
|
("095700", "제넥신", "KOSDAQ"),
|
||||||
|
("214150", "클래시스", "KOSDAQ"),
|
||||||
|
("121600", "나노신소재", "KOSDAQ"),
|
||||||
|
("064550", "바이오니아", "KOSDAQ"),
|
||||||
|
("304100", "솔트룩스", "KOSDAQ"),
|
||||||
|
("078340", "컴투스", "KOSDAQ"),
|
||||||
|
("226330", "신테카바이오", "KOSDAQ"),
|
||||||
|
("215000", "골프존", "KOSDAQ"),
|
||||||
|
("950140", "잉글우드랩", "KOSDAQ"),
|
||||||
|
("328130", "루닛", "KOSDAQ"),
|
||||||
|
("000250", "삼천당제약", "KOSDAQ"),
|
||||||
|
("237690", "에스티팜", "KOSDAQ"),
|
||||||
|
("196300", "쇼박스", "KOSDAQ"),
|
||||||
|
("215600", "신라젠", "KOSDAQ"),
|
||||||
|
("357580", "엠로", "KOSDAQ"),
|
||||||
|
("403870", "HPSP", "KOSDAQ"),
|
||||||
|
("078160", "메디포스트", "KOSDAQ"),
|
||||||
|
("141080", "리가켐바이오", "KOSDAQ"),
|
||||||
|
("306200", "세아메카닉스", "KOSDAQ"),
|
||||||
|
("213420", "덕산네오룩스", "KOSDAQ"),
|
||||||
|
("085660", "차바이오텍", "KOSDAQ"),
|
||||||
|
("950130", "엑세스바이오", "KOSDAQ"),
|
||||||
|
("950170", "JTC", "KOSDAQ"),
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user