feat(ws): 키움 WS 시세 마이그레이션 검증 인프라

KIS WS(41 한도) → 키움 WS(100 한도) 본격 전환 전, 두 소스를 동시 운영해
가격 일치성을 데이터로 검증하기 위한 인프라.

신규
- kiwoom_ws.py: 키움 WS 클라이언트 (KIS WS 와 동일 get_price 인터페이스, 메모리 dict 캐시)
- kis_trader/network/ws_validator.py: 5초마다 KIS↔키움 가격 비교, ws_price_validation 테이블에 1행 INSERT, |diff|≥WARN_PCT 시 WARN 로그
- test_kiwoom_ws.py: 키움 WS 단독 동작 확인 스크립트 (토큰/LOGIN/REG/시세)

수정
- database.py: ws_price_validation 테이블 + ENV 키 (WS_PROVIDER, WS_VALIDATION_INTERVAL_SEC, WS_VALIDATION_DIFF_WARN_PCT) + insert_ws_price_validation/get_ws_validation_stats 헬퍼
- kis_trader/main.py: WS_PROVIDER=kis_with_validation 시 키움 WS + Validator 백그라운드 기동, 종료 시 정리

운영 영향: 0. 매매·시세 의사결정은 항상 KIS WS만 사용. 키움 WS는 백그라운드 비교 기록만.
적용: DB env_config 에 WS_PROVIDER=kis_with_validation INSERT 후 봇 재시작.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-05 21:28:22 +09:00
parent f61c471aac
commit 3fa9eb9bf7
5 changed files with 1087 additions and 1 deletions

View File

@@ -111,6 +111,9 @@ class TradingOrchestrator:
# 시장 급락 서킷브레이커 (KOSPI/KOSDAQ 지수 폭락 시 신규 매수 전면 차단).
# 지수 조회는 모의 도메인 미지원 가능성 → 실키 우선, 폴백 self.client.
self.market_guard: MarketGuard | None = None
# 시세 마이그레이션 검증 (WS_PROVIDER=kis_with_validation 시만 기동)
self.kiwoom_ws = None
self.ws_validator = None
self.strategies: List[BaseStrategy] = []
self._stop = False
@@ -221,6 +224,9 @@ class TradingOrchestrator:
# 시장 급락 서킷브레이커 매니저 기동 (활성/비활성 무관, 토글로 즉시 전환)
self._start_market_guard()
# 시세 WS 마이그레이션 검증 (WS_PROVIDER 토글에 따라 키움 WS 동시 기동)
self._start_ws_validator()
# 전략 등록 후 쓰레드 시작 (각자 독립 쓰레드)
self._register_strategies()
for strat in self.strategies:
@@ -400,6 +406,77 @@ class TradingOrchestrator:
logger.error("MarketGuard 기동 실패 (전략은 가드 없이 동작): %s", e)
self.market_guard = None
# ------------------------------------------------------------------
def _start_ws_validator(self) -> None:
"""시세 WS 마이그레이션 검증 — 키움 WS + Validator 백그라운드 기동.
``WS_PROVIDER`` env 토글:
kis_only : 미기동 (기본). 운영 100% 동일.
kis_with_validation : KIS WS 운영 + 키움 WS 검증 동시 (매매 영향 없음).
kiwoom_only : 추후 단계 (검증 통과 후 시세 키움 전환).
매매·시세 의사결정은 항상 KIS WS 만 사용. 키움 WS 는 ``ws_price_validation``
테이블에 기록만 함. 1~2주 누적 후 통계 분석으로 마이그레이션 결정.
"""
from .utils.env import get_env_from_db
provider = (get_env_from_db("WS_PROVIDER", "kis_only") or "kis_only").strip().lower()
if provider == "kis_only":
logger.info(" WS_PROVIDER=kis_only → 키움 WS 검증 비활성")
return
if provider not in ("kis_with_validation",):
logger.warning(
"⚠️ WS_PROVIDER='%s' 알 수 없음 → kis_only 로 폴백 (검증 비활성)",
provider,
)
return
# 키움 키 로드 (kis_ws._get_kiwoom_creds 재사용)
try:
from kis_ws import _get_kiwoom_creds # type: ignore
app_key, app_secret, is_mock = _get_kiwoom_creds(self.db)
except Exception as e:
logger.warning("키움 자격증명 로드 실패 → 검증 비활성: %s", e)
return
if not app_key or not app_secret:
logger.warning("키움 키 미설정 → WS 검증 비활성")
return
# 키움 WS 인스턴스
try:
from kiwoom_ws import KiwoomWebSocketPriceCache # type: ignore
self.kiwoom_ws = KiwoomWebSocketPriceCache(
app_key, app_secret, is_mock=is_mock,
)
if not self.kiwoom_ws.start():
logger.warning("키움 WS 시작 실패 → 검증 비활성")
self.kiwoom_ws = None
return
except Exception as e:
logger.warning("키움 WS 인스턴스 생성 실패: %s", e)
self.kiwoom_ws = None
return
# KIS WS 핸들 가져오기 (WSManager.ws_cache)
kis_ws_handle = getattr(self.ws, "ws_cache", None)
if kis_ws_handle is None:
logger.warning("KIS WS 핸들 미발견 → Validator 비활성 (검증 불가)")
return
# Validator 기동
try:
from .network.ws_validator import WSPriceValidator
self.ws_validator = WSPriceValidator(
kis_ws=kis_ws_handle,
kiwoom_ws=self.kiwoom_ws,
db=self.db,
)
self.ws_validator.start()
logger.info("🔬 [시세 검증 모드] WS_PROVIDER=%s — KIS↔키움 동시 운영", provider)
except Exception as e:
logger.warning("Validator 기동 실패: %s", e)
self.ws_validator = None
def _build_market_client(self) -> KISClient:
"""
시세/조회 전용 실전 KISClient 생성 (init 단계).
@@ -816,6 +893,16 @@ class TradingOrchestrator:
self.ranking_mgr.stop()
except Exception:
pass
try:
if self.ws_validator:
self.ws_validator.stop()
except Exception:
pass
try:
if self.kiwoom_ws:
self.kiwoom_ws.stop()
except Exception:
pass
try:
self.ws.stop()
except Exception: