feat: realtime_hub 장 시간대 게이팅

장외·주말·휴장 시 키움 WS 연결 안 함(재연결 폭주·로그 노이즈 방지).
behive_web._market_phase_state() 지연 import 재사용(active=정규장·NXT만 연결).
장 마감되면 수신 루프가 연결 종료 후 대기 모드(60s 폴링), 개장 시 자동 재연결.
판정 실패 시 보수적으로 연결 유지(fail-open).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hyowons
2026-06-08 14:34:48 +09:00
parent 925014dfd8
commit b813d01cfa
+25 -2
View File
@@ -45,6 +45,8 @@ _BACKOFF_MAX = 30.0
_MAX_CONN_AGE = 25 * 60 _MAX_CONN_AGE = 25 * 60
# recv 타임아웃 — 이 시간 내 아무것도 안 오면 루프 돌며 stop/age 체크 # recv 타임아웃 — 이 시간 내 아무것도 안 오면 루프 돌며 stop/age 체크
_RECV_TIMEOUT = 5.0 _RECV_TIMEOUT = 5.0
# 장 마감 시간대 재확인 주기 — 연결 안 하고 대기하며 이 간격으로 개장 여부 폴링
_CLOSED_POLL_SEC = 60.0
def _ws_url() -> str: def _ws_url() -> str:
@@ -135,19 +137,33 @@ class RealtimeHub:
pass pass
# ---- 내부: 연결 루프 ---- # ---- 내부: 연결 루프 ----
def _market_open(self) -> bool:
"""장 시간대(정규장·NXT) 여부. behive_web._market_phase_state 재사용(지연 import로 순환 회피).
판정 실패 시 보수적으로 True(연결 유지) — 게이팅 때문에 시세가 끊기는 일은 없게."""
try:
import behive_web
return bool(behive_web._market_phase_state().get('active'))
except Exception:
return True
def _run(self) -> None: def _run(self) -> None:
backoff = _BACKOFF_START backoff = _BACKOFF_START
while not self._stop.is_set(): while not self._stop.is_set():
if not self._market_open():
# 장 외(주말·휴장·장외 시간) → 연결 안 하고 대기. 재연결 폭주·로그 노이즈 방지.
self._connected.clear()
self._stop.wait(_CLOSED_POLL_SEC)
continue
try: try:
self._connect_once() self._connect_once()
backoff = _BACKOFF_START # 정상 종료(선제 재연결 등) → 백오프 리셋 backoff = _BACKOFF_START # 정상 종료(선제 재연결·장 마감 등) → 백오프 리셋
except Exception as e: except Exception as e:
self.last_error = f'{type(e).__name__}: {e}' self.last_error = f'{type(e).__name__}: {e}'
self._connected.clear() self._connected.clear()
sys.stderr.write(f'[realtime-hub] 연결 끊김/실패: {self.last_error}\n') sys.stderr.write(f'[realtime-hub] 연결 끊김/실패: {self.last_error}\n')
if self._stop.is_set(): if self._stop.is_set():
break break
time.sleep(backoff) self._stop.wait(backoff)
backoff = min(backoff * 2, _BACKOFF_MAX) backoff = min(backoff * 2, _BACKOFF_MAX)
def _connect_once(self) -> None: def _connect_once(self) -> None:
@@ -175,10 +191,17 @@ class RealtimeHub:
self._send_reg(codes) self._send_reg(codes)
# 3) 수신 루프 # 3) 수신 루프
_last_mkt_check = time.time()
while not self._stop.is_set(): while not self._stop.is_set():
if time.time() - self.connected_at > _MAX_CONN_AGE: if time.time() - self.connected_at > _MAX_CONN_AGE:
sys.stderr.write('[realtime-hub] 토큰 만료 전 선제 재연결\n') sys.stderr.write('[realtime-hub] 토큰 만료 전 선제 재연결\n')
return # 정상 종료 → _run이 즉시 재연결 return # 정상 종료 → _run이 즉시 재연결
# 장 마감되면 연결 종료 → _run이 대기 모드로 전환. 30초마다만 확인(매 recv마다 X).
if time.time() - _last_mkt_check > 30:
_last_mkt_check = time.time()
if not self._market_open():
sys.stderr.write('[realtime-hub] 장 마감 → 연결 종료, 대기 모드\n')
return
try: try:
raw = ws.recv() raw = ws.recv()
except websocket.WebSocketTimeoutException: except websocket.WebSocketTimeoutException: