이번에 들어간 내용
한투 호가 = 2번째 앱키 전용 키 없거나 start 실패 시 메인에 H0STASP0 안 붙임. 운영설정 WS_ORDERBOOK_SAVE_KIS 빨간 danger. LS RAM 합집합 후보∪보유∪영구∪grace. sync_targets와 split reconcile 둘 다. 틱 DB 영구 게이트는 그대로. 분봉 쓰레기 → 다음 소스 봉 통째 그 분 틱 0건이거나 전부 봉끝 대비 LIVE_FEED_FALLBACK_MAX_AGE_SEC 초과면 구멍. 메인 WS → 2차 → LS → REST → rollup. CANDLE_GARBAGE_FALLBACK 기본 true. 파일: feed_fallback.py(신규), ws_manager.py, kis_ws.py, candle_series.py, bt_candle_source.py, live_config_schema.py, database.py, 스모크, MD 2개. 같은 ws_manager/database/kis_ws/live_config에는 직전 커밋 이후 쌓여 있던 시세 폴백·ENV 키 정리도 같이 들어갔습니다. 파일 단위로 나눌 수 없어서입니다.
This commit is contained in:
@@ -191,10 +191,10 @@ class KISOverseasWebSocket:
|
||||
if tr_key and self._connected and self._ws:
|
||||
self._send_sub_msg(tr_key, subscribe=False)
|
||||
|
||||
def get_price(self, code: str, max_age_sec: float = 1200.0) -> Optional[Dict]:
|
||||
def get_price(self, code: str, max_age_sec: Optional[float] = None) -> Optional[Dict]:
|
||||
"""
|
||||
해외 캐시에서 가격 dict 반환 (inquire_price 호환 키).
|
||||
※ 기본 max_age 1200초(20분): 미국 실시간 미신청 시 15분 지연 + 거래 공백 고려.
|
||||
max_age_sec is None 이면 나이 무시(마지막 RAM). 체결 공백 ≠ 가격 삭제.
|
||||
"""
|
||||
if not self._available or not self._connected:
|
||||
return None
|
||||
@@ -203,10 +203,13 @@ class KISOverseasWebSocket:
|
||||
entry = self._cache.get(code)
|
||||
if not entry:
|
||||
return None
|
||||
if (time.time() - entry.get("ts", 0)) > max_age_sec:
|
||||
if max_age_sec is not None and (time.time() - entry.get("ts", 0)) > max_age_sec:
|
||||
return None
|
||||
return entry.get("data")
|
||||
|
||||
def get_price_last(self, code: str) -> Optional[Dict]:
|
||||
return self.get_price(code, max_age_sec=None)
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
return bool(self._available and self._connected and self._running)
|
||||
@@ -236,7 +239,38 @@ class KISOverseasWebSocket:
|
||||
logger.info("✅ KIS 해외 WebSocket 수신 스레드 시작 (HDFSCNT0 | url=%s)", self._ws_url)
|
||||
return True
|
||||
|
||||
def _unsubscribe_all_before_close(self, reason: str = "", *, clear_ram: bool = False) -> int:
|
||||
"""세션 close 전 HDFSCNT0 구독 해제. 양보 시 RAM 심볼은 유지."""
|
||||
if not self._ws:
|
||||
if clear_ram:
|
||||
with self._sym_lock:
|
||||
self._symbols.clear()
|
||||
self._key2code.clear()
|
||||
return 0
|
||||
with self._sym_lock:
|
||||
pairs = list(self._symbols.items())
|
||||
if clear_ram:
|
||||
self._symbols.clear()
|
||||
self._key2code.clear()
|
||||
if not pairs:
|
||||
return 0
|
||||
gap = max(0.0, float(get_env_float("KIS_WS_SUBSCRIBE_GAP_MIN_SEC", 0.08)))
|
||||
n = 0
|
||||
for i, (code, tr_key) in enumerate(pairs):
|
||||
if i > 0 and gap > 0:
|
||||
time.sleep(gap)
|
||||
self._send_sub_msg(tr_key, subscribe=False)
|
||||
n += 1
|
||||
if gap > 0:
|
||||
time.sleep(gap)
|
||||
logger.info("✅ 해외 세션 종료 전 구독 해제 %d종목 (%s)", n, reason or "-")
|
||||
return n
|
||||
|
||||
def stop(self) -> None:
|
||||
try:
|
||||
self._unsubscribe_all_before_close(reason="stop", clear_ram=True)
|
||||
except Exception as e:
|
||||
logger.warning("해외 WS 종료 전 구독 해제 실패: %s", e)
|
||||
self._running = False
|
||||
self._connected = False
|
||||
if self._ws:
|
||||
@@ -426,6 +460,10 @@ class KISOverseasWebSocket:
|
||||
ws = self._ws
|
||||
if ws is None:
|
||||
return
|
||||
try:
|
||||
self._unsubscribe_all_before_close(reason=reason, clear_ram=False)
|
||||
except Exception as exc:
|
||||
logger.warning("해외 세션 양보 전 구독 해제 실패: %s", exc)
|
||||
try:
|
||||
logger.info("🔌 [해외WS] 세션 양보 close (%s)", reason)
|
||||
ws.close()
|
||||
|
||||
Reference in New Issue
Block a user