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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user