ls증권 히스토리 구독 넣음
This commit is contained in:
@@ -160,6 +160,8 @@ class KiwoomWebSocketPriceCache:
|
||||
# 메모리 캐시 — KIS WS 와 동일 포맷 (data + ts)
|
||||
self._cache: Dict[str, Dict] = {}
|
||||
self._cache_lock = threading.Lock()
|
||||
self._price_listeners: list = []
|
||||
self._price_listener_lock = threading.Lock()
|
||||
self._orderbook_cache = OrderbookCache() if OrderbookCache else None
|
||||
self._program_cache = ProgramCache() if ProgramCache else None
|
||||
|
||||
@@ -461,6 +463,32 @@ class KiwoomWebSocketPriceCache:
|
||||
return None
|
||||
return self._program_cache.get(code, max_age_sec=max_age_sec)
|
||||
|
||||
def add_price_listener(self, callback) -> None:
|
||||
"""현재가 갱신 콜백 등록. callback(code, price, data_dict)."""
|
||||
if callback is None:
|
||||
return
|
||||
with self._price_listener_lock:
|
||||
if callback not in self._price_listeners:
|
||||
self._price_listeners.append(callback)
|
||||
|
||||
def remove_price_listener(self, callback) -> None:
|
||||
if callback is None:
|
||||
return
|
||||
with self._price_listener_lock:
|
||||
try:
|
||||
self._price_listeners.remove(callback)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def _emit_price_listeners(self, code: str, price: float, data: Dict) -> None:
|
||||
with self._price_listener_lock:
|
||||
listeners = list(self._price_listeners)
|
||||
for cb in listeners:
|
||||
try:
|
||||
cb(code, price, data)
|
||||
except Exception as ex:
|
||||
logger.debug("price_listener 예외 %s: %s", code, ex)
|
||||
|
||||
def get_price(self, code: str, max_age_sec: float = 5.0) -> Optional[Dict]:
|
||||
"""KIS WS ``get_price`` 와 동일 포맷 반환.
|
||||
|
||||
@@ -593,6 +621,9 @@ class KiwoomWebSocketPriceCache:
|
||||
rm = msg.get("return_msg", "")
|
||||
if rc == 0:
|
||||
self._authenticated = True
|
||||
# 안정 LOGIN 성공 → 재연결 카운터 리셋 (주말 Bye 후 소모분 복구)
|
||||
self._reconnect_count = 0
|
||||
self._reconnect_delay = self.RECONNECT_BASE_DELAY_SEC
|
||||
logger.info("✅ 키움 WS LOGIN OK")
|
||||
# 디바운스 타이머 취소 — LOGIN 직후 일괄 REG 와 이중 전송 방지
|
||||
with self._reg_timer_lock:
|
||||
@@ -612,6 +643,11 @@ class KiwoomWebSocketPriceCache:
|
||||
self._fire_login_callbacks(ws)
|
||||
else:
|
||||
logger.warning("❌ 키움 WS LOGIN 실패 rc=%s msg=%s", rc, rm)
|
||||
# 805004 등 서버 토큰 거부 → 캐시 만료 전이라도 무효화 후 재발급
|
||||
if self._is_token_rejected_login(rc, rm):
|
||||
self._invalidate_token_cache(
|
||||
"LOGIN rc=%s msg=%s" % (rc, rm),
|
||||
)
|
||||
try:
|
||||
ws.close()
|
||||
except Exception:
|
||||
@@ -695,6 +731,7 @@ class KiwoomWebSocketPriceCache:
|
||||
}
|
||||
with self._cache_lock:
|
||||
self._cache[code] = {"data": data_compat, "ts": time.time()}
|
||||
self._emit_price_listeners(code, float(price), data_compat)
|
||||
|
||||
# tick_time / tick_vol — CandleAggregator·TickRecorder 공용 (보유-only도 recorder 수집)
|
||||
tt_raw = str(values.get(self.FID_TICK_TIME, "") or "").strip()
|
||||
@@ -804,6 +841,27 @@ class KiwoomWebSocketPriceCache:
|
||||
return None
|
||||
return _get_kiwoom_token_cached(self.app_key, self.app_secret, self.is_mock)
|
||||
|
||||
@staticmethod
|
||||
def _is_token_rejected_login(rc: Any, rm: Any) -> bool:
|
||||
"""WS LOGIN 응답이 '토큰 무효'인지 — 캐시 invalidate 대상."""
|
||||
try:
|
||||
if int(rc) == 805004:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
m = str(rm or "")
|
||||
return ("Token이 유효하지 않습니다" in m) or ("토큰이 유효하지 않습니다" in m)
|
||||
|
||||
def _invalidate_token_cache(self, reason: str) -> None:
|
||||
try:
|
||||
from .kis_ws import _invalidate_kiwoom_token_cached
|
||||
except ImportError:
|
||||
logger.warning("kis_ws._invalidate_kiwoom_token_cached import 실패")
|
||||
return
|
||||
_invalidate_kiwoom_token_cached(
|
||||
self.app_key, self.app_secret, self.is_mock, reason=reason,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 내부: 재연결 백오프 (KIS WS 와 동일 정책)
|
||||
# ------------------------------------------------------------------
|
||||
@@ -825,10 +883,27 @@ class KiwoomWebSocketPriceCache:
|
||||
time.sleep(max(60.0, wait))
|
||||
return
|
||||
|
||||
# 총 한도 초과 → 비활성
|
||||
# 총 한도 초과 — 기본은 긴 쿨다운 후 카운터 리셋 (주말 토큰거부 연타로
|
||||
# 월요일까지 영구 OFF 되면 SCALP/MOMENTUM 조건식 REAL 이 끊김).
|
||||
# 응급으로만 KIWOOM_WS_HARD_DISABLE_ON_MAX_RECONNECT=true.
|
||||
if self._reconnect_count >= self.MAX_RECONNECT_ATTEMPTS:
|
||||
logger.warning("⛔ 키움 WS 총 재연결 한도 초과 → 자동 비활성")
|
||||
self._running = False
|
||||
if get_env_bool("KIWOOM_WS_HARD_DISABLE_ON_MAX_RECONNECT", False):
|
||||
logger.warning("⛔ 키움 WS 총 재연결 한도 초과 → 자동 비활성")
|
||||
self._running = False
|
||||
return
|
||||
cool = float(get_env_float("KIWOOM_WS_MAX_RECONNECT_COOLDOWN_SEC", 1800.0))
|
||||
cool = max(60.0, min(cool, 86400.0))
|
||||
logger.warning(
|
||||
"⚠️ 키움 WS 총 재연결 한도(#%d) → %.0fs 쿨다운 후 카운터 리셋 "
|
||||
"(영구 OFF 안 함 · HARD_DISABLE=true 시만 비활성)",
|
||||
self._reconnect_count, cool,
|
||||
)
|
||||
# 쿨다운 전 토큰 캐시 비우기 — 죽은 토큰 재전송 루프 차단
|
||||
self._invalidate_token_cache("max reconnect cooldown")
|
||||
time.sleep(cool)
|
||||
self._reconnect_count = 0
|
||||
self._reconnect_delay = self.RECONNECT_BASE_DELAY_SEC
|
||||
self._reconnect_times.clear()
|
||||
return
|
||||
|
||||
delay = min(self._reconnect_delay, self.RECONNECT_MAX_DELAY_SEC)
|
||||
|
||||
Reference in New Issue
Block a user