#!/usr/bin/env python3 """ _test_kiwoom_condition_list.py — 키움 웹소켓 조건검색 '목록조회'(ka10171 / CNSRLST) 단독 테스트 ================================================================================================== [목적] 실매를 돌리지 않고, 키움 신형 REST/WS API 로 서버에 저장된 조건검색식 목록을 웹소켓으로 가져올 수 있는지만 확인한다. (유니버스 전환의 전제조건 검증) [흐름] 1) DB(env_config)에서 키움 앱키/시크릿/모의여부 로드 (봇과 동일 키) 2) KiwoomTokenManager 로 access_token 발급 (au10001) 3) wss://api.kiwoom.com:10000/api/dostk/websocket 연결 4) {"trnm":"LOGIN","token":...} 인증 5) LOGIN OK → {"trnm":"CNSRLST"} 전송 6) CNSRLST 응답의 조건식 목록(seq, name) 출력 후 종료 [주의] - 읽기 전용(조회)만 한다. 실시간 등록(CNSRREQ)·주문은 하지 않는다. - 실제 응답 원문(raw)을 그대로 찍어, 필드명이 문서와 달라도 눈으로 확인 가능. """ import json import os import sys import threading import time sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from kis_trader.utils.env import get_env_from_db from kis_trader.ws.kis_ws import _get_kiwoom_token_cached def _load_kiwoom_creds(): """봇과 동일한 우선순위로 키움 키를 로드 (REAL → LEGACY, MOCK 토글).""" kw_mock_raw = (get_env_from_db("KIWOOM_MOCK", "") or "").strip().lower() is_mock = kw_mock_raw in ("1", "true", "y", "yes", "on") if is_mock: key = (get_env_from_db("KIWOOM_APP_KEY_MOCK", "") or "").strip() secret = (get_env_from_db("KIWOOM_APP_SECRET_MOCK", "") or "").strip() else: key = (get_env_from_db("KIWOOM_APP_KEY_REAL", "") or "").strip() secret = (get_env_from_db("KIWOOM_APP_SECRET_REAL", "") or "").strip() # 레거시 단일 필드 폴백 if not key or not secret: key = key or (get_env_from_db("KIWOOM_APP_KEY", "") or "").strip() secret = secret or (get_env_from_db("KIWOOM_APP_SECRET", "") or "").strip() return key, secret, is_mock def main() -> int: try: import websocket # websocket-client except Exception as e: print(f"❌ websocket-client 미설치: {e} (pip install websocket-client)") return 1 key, secret, is_mock = _load_kiwoom_creds() if not key or not secret: print("❌ 키움 앱키/시크릿 미설정 — env_config 의 KIWOOM_APP_KEY_REAL/SECRET_REAL 확인") return 1 print(f"🔑 키움 키 로드 OK (mock={is_mock}, key 앞8자={key[:8]}…)") token = _get_kiwoom_token_cached(key, secret, is_mock) if not token: print("❌ 키움 토큰 발급 실패 (au10001)") return 1 print(f"🎫 토큰 발급 OK (앞8자={token[:8]}…)") url = ( get_env_from_db("KIWOOM_WS_URL_MOCK", "wss://mockapi.kiwoom.com:10000/api/dostk/websocket") if is_mock else get_env_from_db("KIWOOM_WS_URL_REAL", "wss://api.kiwoom.com:10000/api/dostk/websocket") ) print(f"🌐 WS 연결 시도: {url}") state = {"done": False, "conditions": None, "error": None} def on_open(ws): try: ws.send(json.dumps({"trnm": "LOGIN", "token": token})) print("📡 LOGIN 발송") except Exception as e: state["error"] = f"LOGIN 발송 실패: {e}" ws.close() def on_message(ws, message): try: data = json.loads(message) except Exception: print(f"📥 (raw, non-json) {message[:200]}") return trnm = data.get("trnm") # 서버 PING 은 그대로 echo (연결 유지) if trnm == "PING": try: ws.send(message) except Exception: pass return if trnm == "LOGIN": rc = str(data.get("return_code")) if rc in ("0", "0.0"): print("✅ LOGIN OK → CNSRLST(조건검색 목록조회) 요청") ws.send(json.dumps({"trnm": "CNSRLST"})) else: state["error"] = f"LOGIN 실패 rc={rc} msg={data.get('return_msg')}" ws.close() return if trnm == "CNSRLST": print("\n===== CNSRLST 응답 원문 =====") print(json.dumps(data, ensure_ascii=False, indent=2)) state["conditions"] = data.get("data") or [] state["done"] = True ws.close() return # 그 외 메시지도 원문 표시(디버깅용) print(f"📥 기타 메시지: {json.dumps(data, ensure_ascii=False)[:300]}") def on_error(ws, err): state["error"] = f"WS 오류: {err}" def on_close(ws, code, msg): state["done"] = True ws = websocket.WebSocketApp( url, on_open=on_open, on_message=on_message, on_error=on_error, on_close=on_close, ) th = threading.Thread(target=ws.run_forever, kwargs={"ping_interval": 0}, daemon=True) th.start() # 최대 20초 대기 deadline = time.time() + 20 while time.time() < deadline and not state["done"]: time.sleep(0.2) try: ws.close() except Exception: pass if state["error"]: print(f"\n🚨 {state['error']}") return 2 conds = state["conditions"] if conds is None: print("\n⏱️ 응답 없이 타임아웃 — 연결/인증 로그 확인 필요") return 3 print(f"\n📋 조건검색식 {len(conds)}개:") momentum_seq = None for c in conds: # 응답 포맷: [seq, name] 배열 또는 {"seq":..,"name":..} dict 둘 다 허용 if isinstance(c, (list, tuple)): seq, name = (c[0] if len(c) > 0 else ""), (c[1] if len(c) > 1 else "") else: seq, name = c.get("seq"), c.get("name") mark = "" if str(name).strip().lower() == "momentum": momentum_seq = seq mark = " ← momentum" print(f" seq={str(seq):>4} name={name}{mark}") if momentum_seq is not None: print(f"\n🎯 'momentum' 조건식 발견: seq={momentum_seq}") else: print("\n⚠️ 이름이 정확히 'momentum' 인 조건식은 목록에 없음(이름 확인 필요)") print("\n🎉 조건검색 목록조회(웹소켓) 성공 — 키움 유니버스 전환 전제조건 충족") return 0 if __name__ == "__main__": sys.exit(main())