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:
2026-07-06 01:27:00 +09:00
parent d8ba01afa4
commit 61c72a8a4c
171 changed files with 176914 additions and 7329 deletions

View File

@@ -82,7 +82,10 @@ class ConditionSearchManager:
if poll_interval_sec is not None
else get_env_int("CONDITION_POLL_INTERVAL_SEC", 10)
)
self.history_enabled = get_env_bool("CONDITION_HISTORY_SAVE", True)
self.history_enabled = get_env_bool(
"CONDITION_HISTORY_SAVE",
get_env_bool("UNIVERSE_HISTORY_SAVE", True),
)
# EXIT grace period: 한 번 빠진 종목을 N초간 universe 에 keep.
# 단발성 EXIT/RE-ENTER 회전을 흡수해 WS 구독 해제 → 캐시·갭보정 리셋
# → resubscribe 후 데이터 부족으로 매수 시그널 못내는 사이클을 차단.
@@ -113,6 +116,7 @@ class ConditionSearchManager:
# 상태
self._current: Dict[str, Set[str]] = {} # strategy_id → code set
self._current_order: Dict[str, List[str]] = {} # strategy_id → 매수 우선순위(HTS 응답 순)
self._name_map: Dict[str, str] = {} # code → name (전역)
# 초기 tick 에서 "빈 set → 첫 결과" 를 변동으로 간주해 1회는 저장
self._initialized: Set[str] = set()
@@ -180,7 +184,9 @@ class ConditionSearchManager:
"""BaseStrategy._load_candidates 와 호환되는 dict 리스트 반환."""
sid = (strategy_id or "").upper()
with self._lock:
codes = list(self._current.get(sid, set()))
codes = list(self._current_order.get(sid) or [])
if not codes:
codes = list(self._current.get(sid, set()))
nm = dict(self._name_map)
# 전략별 기본 필터 플래그는 True 로 열어둔다 (전략 쪽 _candidate_filter 가 판단)
out = []
@@ -190,6 +196,7 @@ class ConditionSearchManager:
"name": nm.get(c, c),
"scalp_on": True,
"tail_on": True,
"updow_on": True,
"score": 0.0,
"price": 0.0,
})
@@ -264,10 +271,15 @@ class ConditionSearchManager:
time.sleep(0.5)
def _tick_all(self) -> None:
from ..utils.universe_source import universe_source_active
# 같은 seq 를 공유하는 전략이 있으면 REST 1회만 호출 (캐시)
seq_cache: Dict[str, List[Dict]] = {}
for cfg in self._configs:
sid = cfg["strategy_id"]
# UNIVERSE_SOURCE != condition 이면 KIS REST 스킵 (kiwoom_condition 등)
if not universe_source_active(sid, "condition"):
continue
seq = cfg["seq"]
if seq in seq_cache:
rows = seq_cache[seq]
@@ -333,6 +345,11 @@ class ConditionSearchManager:
enters = new_set - prev
exits = prev - new_set
self._current[strategy_id] = new_set
self._current_order[strategy_id] = self._build_ordered_universe(
rows=rows,
new_set=new_set,
prev_order=list(self._current_order.get(strategy_id) or []),
)
self._initialized.add(strategy_id)
for c, n in new_names.items():
self._name_map[c] = n
@@ -367,21 +384,54 @@ class ConditionSearchManager:
# (effective universe 기준 — 봇이 실제로 보던 universe 가 그대로 기록됨)
# 변동 없는 tick 은 공간 절약 위해 skip
if changed or first_tick:
self._save_snapshot(strategy_id, new_set, new_names)
with self._lock:
ordered = list(self._current_order.get(strategy_id) or [])
self._save_snapshot(strategy_id, ordered, new_names)
@staticmethod
def _build_ordered_universe(
*,
rows: List[Dict],
new_set: Set[str],
prev_order: List[str],
) -> List[str]:
"""
실매 ``get_candidates_for`` / DB history 공통 순서.
1) HTS/KIS 조건검색 응답 순서 2) grace 유지(이전 순서) 3) 나머지
"""
ordered: List[str] = []
seen: Set[str] = set()
for r in rows:
c = str(r.get("code") or "").strip()
if c and c in new_set and c not in seen:
seen.add(c)
ordered.append(c)
for c in prev_order:
if c in new_set and c not in seen:
seen.add(c)
ordered.append(c)
for c in new_set:
if c not in seen:
ordered.append(c)
return ordered
def _save_snapshot(
self,
strategy_id: str,
codes: Set[str],
codes_ordered: List[str],
names: Dict[str, str],
) -> None:
"""변동이 감지된 tick 의 풀 유니버스 스냅샷 저장."""
"""변동이 감지된 tick 의 풀 유니버스 스냅샷 저장 (HTS 응답 순서 유지)."""
if not (self.history_enabled and self.db is not None):
return
# 유니버스가 비어있어도 "비었다" 는 사실을 기록해야 백테스트에서 재현 가능.
# 단, 한 번도 결과를 못 받은 상태(첫 호출 실패 등)는 저장 X.
event_time = dt.now().strftime("%Y-%m-%d %H:%M:%S")
items = [{"code": c, "name": names.get(c, c)} for c in sorted(codes)]
items = [
{"code": c, "name": names.get(c, c)}
for c in codes_ordered
if c
]
try:
n = self.db.insert_condition_universe_snapshot(
strategy_id=strategy_id,