ls증권 히스토리 구독 넣음

This commit is contained in:
Your Name
2026-07-30 18:05:07 +09:00
parent 61bec4bd1d
commit 67eab24603
1593 changed files with 135733 additions and 1232 deletions

View File

@@ -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)