fix(ws): KIS/키움 WS·조건검색 안정화 및 매도시세 폴백

WS 매니저 spill·조건검색 CNSR 회복·kiwoom_ws_diag 진단 경로를 보강한다.
live_sell_price stale RAM/REST 폴백 정합을 유지한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Your Name
2026-08-28 16:46:37 +09:00
parent 1387fbdf47
commit d737deb47c
6 changed files with 263 additions and 20 deletions

View File

@@ -93,6 +93,8 @@ except ImportError:
ProgramCache = None # type: ignore[misc, assignment]
ProgramSnapshot = None # type: ignore[misc, assignment]
from .kiwoom_ws_diag import log_kiwoom_api_msg, log_kiwoom_ws
# ──────────────────────────────────────────────────────────────────────
# 메인 클래스
@@ -364,11 +366,25 @@ class KiwoomWebSocketPriceCache:
def send_json(self, msg: dict) -> bool:
"""인증된 WS 에 JSON 전송 (조건검색 CNSRREQ 등)."""
if not self.is_authenticated() or not self._ws:
log_kiwoom_ws(
logger, "send_skip",
level="warning",
trnm=str(msg.get("trnm") or ""),
extra="미인증 또는 ws=None",
force=True,
)
return False
try:
self._ws.send(json.dumps(msg))
return True
except Exception as e:
log_kiwoom_ws(
logger, "send_fail",
level="warning",
trnm=str(msg.get("trnm") or ""),
extra=str(e),
force=True,
)
logger.debug("키움 WS send_json 실패: %s", e)
return False
@@ -667,6 +683,9 @@ class KiwoomWebSocketPriceCache:
trnm = msg.get("trnm", "")
if trnm in ("CNSRLST", "CNSRREQ", "CNSRCLR", "REG", "REMOVE", "LOGIN"):
log_kiwoom_api_msg(logger, str(trnm or "?"), msg)
if trnm == "PING":
# 키움 PING → 그대로 echo (서버 정책)
try:
@@ -728,7 +747,10 @@ class KiwoomWebSocketPriceCache:
if trnm == "REG":
rc = msg.get("return_code")
if rc != 0:
logger.warning("⚠️ 키움 WS REG 실패: %s", msg.get("return_msg", ""))
logger.warning(
"⚠️ 키움 WS REG 실패 rc=%s msg=%s",
rc, msg.get("return_msg", ""),
)
return
if trnm == "REMOVE":
@@ -955,11 +977,25 @@ class KiwoomWebSocketPriceCache:
def _on_error(self, ws, error) -> None:
self._connected = False
self._authenticated = False
err_s = str(error or "")
log_kiwoom_ws(
logger, "on_error",
level="warning",
extra=err_s,
force=True,
)
logger.warning("⚠️ 키움 WS 오류: %s", error)
def _on_close(self, ws, close_status_code, close_msg) -> None:
self._connected = False
self._authenticated = False
log_kiwoom_ws(
logger, "on_close",
level="info",
rc=close_status_code,
msg=close_msg or "",
force=True,
)
logger.info("🔌 키움 WS 연결 종료 (code=%s msg=%s)",
close_status_code, close_msg or "")
@@ -1079,6 +1115,12 @@ class KiwoomWebSocketPriceCache:
self._reconnect_times = [t for t in self._reconnect_times if t > cutoff]
if len(self._reconnect_times) >= self.MAX_RECONNECTS_PER_HOUR:
wait = 3600 - (now - self._reconnect_times[0])
log_kiwoom_ws(
logger, "reconnect_hourly_cap",
level="warning",
extra=f"wait={int(wait)}s count={len(self._reconnect_times)}",
force=True,
)
logger.warning("⚠️ 키움 WS 시간당 재연결 한도 초과 → %ds 대기", int(wait))
time.sleep(max(60.0, wait))
return