feat(tests): 신규 키움 웹소켓 조건검색 및 실시간 조건검색 테스트 추가
변경 사항 ---- - _test_kiwoom_condition_list.py: 키움 웹소켓 조건검색 '목록조회' 기능을 단독으로 테스트하는 스크립트 추가 - _test_kiwoom_condition_realtime.py: 'momentum' 조건식을 실시간으로 등록하고 초기 매칭 종목 리스트 및 실시간 편입/이탈을 수신하는 테스트 스크립트 추가 - _verify_columnar_bitid.py, _verify_shared_e2e_breakout.py, _verify_shared_e2e.py: 공유 메모리 및 dict 간의 데이터 일관성을 검증하는 테스트 추가 영향 ---- - 신규 테스트 스크립트 추가로 키움 웹소켓 API의 기능 검증 및 안정성을 높임 - 기존 기능에 대한 영향 없음 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -50,6 +50,52 @@ class SafeRequest:
|
||||
self.timeout_sec = float(timeout_sec)
|
||||
self._last_call_ts: float = 0.0
|
||||
self._throttle_lock = threading.Lock()
|
||||
# ── 계측: 초당 호출 수 / 큐 대기시간 (피크 실측용) ───────────────
|
||||
self._calls_in_window: int = 0
|
||||
self._window_start: float = time.time()
|
||||
self._peak_calls_per_sec: int = 0
|
||||
self._total_calls: int = 0
|
||||
self._total_wait_sec: float = 0.0
|
||||
self._peak_wait_sec: float = 0.0
|
||||
self._rate_limit_hits: int = 0
|
||||
|
||||
def _current_min_interval(self) -> float:
|
||||
"""호출 직전 min_interval 재해석 훅 (서브클래스가 env 핫리로드용으로 오버라이드).
|
||||
|
||||
기본은 고정 ``self.min_interval_sec`` 반환. 오버라이드 시 재시작 없이 반영.
|
||||
"""
|
||||
return self.min_interval_sec
|
||||
|
||||
def _record_call(self, waited: float) -> None:
|
||||
"""계측 누적 — _throttle 내부에서 호출 (lock 보유 상태)."""
|
||||
now = time.time()
|
||||
self._total_calls += 1
|
||||
self._total_wait_sec += waited
|
||||
if waited > self._peak_wait_sec:
|
||||
self._peak_wait_sec = waited
|
||||
# 1초 윈도우 카운트
|
||||
if now - self._window_start >= 1.0:
|
||||
self._window_start = now
|
||||
self._calls_in_window = 1
|
||||
else:
|
||||
self._calls_in_window += 1
|
||||
if self._calls_in_window > self._peak_calls_per_sec:
|
||||
self._peak_calls_per_sec = self._calls_in_window
|
||||
|
||||
def get_throttle_stats(self) -> dict:
|
||||
"""계측 스냅샷 (모니터링·검증용)."""
|
||||
avg_wait = (
|
||||
self._total_wait_sec / self._total_calls
|
||||
if self._total_calls else 0.0
|
||||
)
|
||||
return {
|
||||
"total_calls": self._total_calls,
|
||||
"peak_calls_per_sec": self._peak_calls_per_sec,
|
||||
"avg_wait_sec": round(avg_wait, 4),
|
||||
"peak_wait_sec": round(self._peak_wait_sec, 4),
|
||||
"rate_limit_hits": self._rate_limit_hits,
|
||||
"min_interval_sec": self._current_min_interval(),
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 내부 유틸
|
||||
@@ -57,10 +103,14 @@ class SafeRequest:
|
||||
def _throttle(self) -> None:
|
||||
"""호출 간 최소 간격 보장 (클라이언트 측 429 방지)."""
|
||||
with self._throttle_lock:
|
||||
interval = self._current_min_interval()
|
||||
elapsed = time.time() - self._last_call_ts
|
||||
if elapsed < self.min_interval_sec:
|
||||
time.sleep(self.min_interval_sec - elapsed)
|
||||
waited = 0.0
|
||||
if elapsed < interval:
|
||||
waited = interval - elapsed
|
||||
time.sleep(waited)
|
||||
self._last_call_ts = time.time()
|
||||
self._record_call(waited)
|
||||
|
||||
def _sleep_backoff(self, attempt: int) -> None:
|
||||
"""지수 백오프 + 지터. attempt=1,2,3..."""
|
||||
@@ -111,6 +161,8 @@ class SafeRequest:
|
||||
|
||||
# [1] 재시도 대상 HTTP 상태
|
||||
if resp.status_code in self.RETRYABLE_STATUSES:
|
||||
if resp.status_code == 429:
|
||||
self._rate_limit_hits += 1
|
||||
logger.warning(
|
||||
"HTTP %d on %s %s (%d/%d) → 백오프 후 재시도",
|
||||
resp.status_code, method.upper(), url, attempt, self.max_retries,
|
||||
@@ -125,6 +177,7 @@ class SafeRequest:
|
||||
except Exception:
|
||||
body = None
|
||||
if body and self._is_kis_rate_limited(body):
|
||||
self._rate_limit_hits += 1
|
||||
logger.warning(
|
||||
"KIS rate-limit %s on %s (%d/%d) → 백오프 후 재시도",
|
||||
body.get("msg_cd"), url, attempt, self.max_retries,
|
||||
|
||||
Reference in New Issue
Block a user