From b813d01cfa71125e8f95583f3ee4032f2b87d537 Mon Sep 17 00:00:00 2001 From: hyowons Date: Mon, 8 Jun 2026 14:34:48 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20realtime=5Fhub=20=EC=9E=A5=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=EB=8C=80=20=EA=B2=8C=EC=9D=B4=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 장외·주말·휴장 시 키움 WS 연결 안 함(재연결 폭주·로그 노이즈 방지). behive_web._market_phase_state() 지연 import 재사용(active=정규장·NXT만 연결). 장 마감되면 수신 루프가 연결 종료 후 대기 모드(60s 폴링), 개장 시 자동 재연결. 판정 실패 시 보수적으로 연결 유지(fail-open). Co-Authored-By: Claude Opus 4.8 --- .../stock/workspace/scripts/realtime_hub.py | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/agents/stock/workspace/scripts/realtime_hub.py b/agents/stock/workspace/scripts/realtime_hub.py index 8aacbf7..c489536 100644 --- a/agents/stock/workspace/scripts/realtime_hub.py +++ b/agents/stock/workspace/scripts/realtime_hub.py @@ -45,6 +45,8 @@ _BACKOFF_MAX = 30.0 _MAX_CONN_AGE = 25 * 60 # recv 타임아웃 — 이 시간 내 아무것도 안 오면 루프 돌며 stop/age 체크 _RECV_TIMEOUT = 5.0 +# 장 마감 시간대 재확인 주기 — 연결 안 하고 대기하며 이 간격으로 개장 여부 폴링 +_CLOSED_POLL_SEC = 60.0 def _ws_url() -> str: @@ -135,19 +137,33 @@ class RealtimeHub: 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: backoff = _BACKOFF_START while not self._stop.is_set(): + if not self._market_open(): + # 장 외(주말·휴장·장외 시간) → 연결 안 하고 대기. 재연결 폭주·로그 노이즈 방지. + self._connected.clear() + self._stop.wait(_CLOSED_POLL_SEC) + continue try: self._connect_once() - backoff = _BACKOFF_START # 정상 종료(선제 재연결 등) → 백오프 리셋 + backoff = _BACKOFF_START # 정상 종료(선제 재연결·장 마감 등) → 백오프 리셋 except Exception as e: self.last_error = f'{type(e).__name__}: {e}' self._connected.clear() sys.stderr.write(f'[realtime-hub] 연결 끊김/실패: {self.last_error}\n') if self._stop.is_set(): break - time.sleep(backoff) + self._stop.wait(backoff) backoff = min(backoff * 2, _BACKOFF_MAX) def _connect_once(self) -> None: @@ -175,10 +191,17 @@ class RealtimeHub: self._send_reg(codes) # 3) 수신 루프 + _last_mkt_check = time.time() while not self._stop.is_set(): if time.time() - self.connected_at > _MAX_CONN_AGE: sys.stderr.write('[realtime-hub] 토큰 만료 전 선제 재연결\n') 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: raw = ws.recv() except websocket.WebSocketTimeoutException: