fix(시세): LS spill-only·토큰 파일캐시·호가 snap_time 3초컷

- _feed_fallback 미러 OFF, LS cap/grace/hold RAM을 KIS·키움 spill과 정합
- LS 접근토큰 .ls_token_cache_*.json (재시작 재사용, revoke 루프 없음)
- 호가 RAM을 틱과 동일 LIVE_FEED_FALLBACK(snap_time)로 컷, 필터 max_age=0은 유지
- 익절 지정가 로그에 실제 호가 벤더(kis/kiwoom/ls 1·2·3차) 표기

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Your Name
2026-08-27 15:21:21 +09:00
parent ba2f9f0953
commit 5e44b86f8b
24 changed files with 1923 additions and 203 deletions

View File

@@ -22,7 +22,7 @@ KIS 공식 스펙:
'해외주식 실시간시세' 신청이 안 돼 있으면 HDFSCNT0 는 **15분 지연**으로 옵니다.
15분봉 전략에선 신청 필수. 미신청 시에도 동작은 하나 데이터가 지연됩니다.
KIS 안티-어뷰즈 정책 준수: 재연결 횟수/시간 제한 + 지수 백오프 (국내 수신기와 동일 사상).
KIS 안티-어뷰즈 정책 준수: 재연결 횟수/시간 제한 + 단계 대기 (WS_RECONNECT_DELAY_SECS, 기본 1,3,5,7,10초).
"""
from __future__ import annotations
@@ -37,6 +37,8 @@ import requests
logger = logging.getLogger("KISWebSocketOverseas")
from .ws_reconnect_backoff import ws_reconnect_delay_for_attempt
try:
from kis_trader.utils.env import get_env_from_db, get_env_bool, get_env_int, get_env_float
except ImportError: # 단독 실행 fallback
@@ -83,8 +85,7 @@ class KISOverseasWebSocket:
MAX_SUBSCRIPTIONS = 41
MAX_RECONNECT_ATTEMPTS = 10
MAX_RECONNECTS_PER_HOUR = 6
RECONNECT_BASE_DELAY_SEC = 5.0
RECONNECT_MAX_DELAY_SEC = 300.0
# 재연결 sleep — ws_reconnect_backoff (기본 1,3,5,7,10초)
STABLE_CONN_RESET_SEC = 300.0
APPROVAL_KEY_CACHE_SEC = 82800
@@ -124,7 +125,6 @@ class KISOverseasWebSocket:
self._reconnect_count = 0
self._reconnect_times: list = []
self._reconnect_delay = self.RECONNECT_BASE_DELAY_SEC
self._last_connect_time = 0.0
self._candle_agg: Optional[Any] = None
@@ -511,7 +511,6 @@ class KISOverseasWebSocket:
)
self._reconnect_count = 0
self._reconnect_times = []
self._reconnect_delay = self.RECONNECT_BASE_DELAY_SEC
_instant_streak = 0
for _ in range(int(wait_sec // 60)):
if not self._running:
@@ -542,7 +541,6 @@ class KISOverseasWebSocket:
and self._reconnect_count > 0):
self._reconnect_count = 0
self._reconnect_times = []
self._reconnect_delay = self.RECONNECT_BASE_DELAY_SEC
self._reconnect_times = [t for t in self._reconnect_times if now - t < 3600]
if len(self._reconnect_times) >= self.MAX_RECONNECTS_PER_HOUR:
@@ -557,8 +555,8 @@ class KISOverseasWebSocket:
# 캐시 approval 재사용만 (force_refresh 금지 — 국내 세션 무효화 방지)
if not self._get_approval_key(force_refresh=False):
time.sleep(self._reconnect_delay)
self._reconnect_delay = min(self._reconnect_delay * 2, self.RECONNECT_MAX_DELAY_SEC)
_attempt = max(1, self._reconnect_count)
time.sleep(ws_reconnect_delay_for_attempt(_attempt))
continue
# 국내가 아직 붙잡고 있으면 짧게 양보 대기 (재발급 없음)
@@ -579,8 +577,9 @@ class KISOverseasWebSocket:
0.0,
float(get_env_float("KIS_WS_RECONNECT_SESSION_WAIT_SEC", 2.0) or 2.0),
)
time.sleep(max(self._reconnect_delay, sess_wait))
self._reconnect_delay = min(self._reconnect_delay * 2, self.RECONNECT_MAX_DELAY_SEC)
_attempt = self._reconnect_count - 1
_delay = ws_reconnect_delay_for_attempt(_attempt)
time.sleep(max(_delay, sess_wait))
_conn_start = time.time()
try:
@@ -607,7 +606,6 @@ class KISOverseasWebSocket:
def _on_open(self, ws) -> None:
self._connected = True
self._reconnect_delay = self.RECONNECT_BASE_DELAY_SEC
self._last_connect_time = time.time()
logger.info("✅ KIS 해외 WebSocket 연결 성공 (HDFSCNT0)")
with self._sym_lock: