refactor: enhance Optuna backtesting framework, optimize orderbook filtering, and update database management utilities.
This commit is contained in:
@@ -86,12 +86,11 @@ class ConditionSearchManager:
|
||||
"CONDITION_HISTORY_SAVE",
|
||||
get_env_bool("UNIVERSE_HISTORY_SAVE", True),
|
||||
)
|
||||
# EXIT grace period: 한 번 빠진 종목을 N초간 universe 에 keep.
|
||||
# 단발성 EXIT/RE-ENTER 회전을 흡수해 WS 구독 해제 → 캐시·갭보정 리셋
|
||||
# → resubscribe 후 데이터 부족으로 매수 시그널 못내는 사이클을 차단.
|
||||
# 기본 0 = 비활성 (기존 동작과 동일). 권장 60.
|
||||
# EXIT grace: 빠진 종목을 N초간 universe 에 keep (0=즉시 pop = 순차 push/pop).
|
||||
# 정합(실매=history)을 깨지 않으려면 history 도 effective(이 목록)을 저장해야 함.
|
||||
# 기본 0. 켜도 history=effective 이면 슬롯정합은 유지됨(실매만 sticky).
|
||||
self._exit_grace_sec = float(get_env_int("CONDITION_EXIT_GRACE_SEC", 0))
|
||||
# strategy_id → {code: first_missing_at_epoch}
|
||||
# strategy_id → {code: first_missing_at_epoch} — grace>0 일 때만 사용
|
||||
self._pending_exit: Dict[str, Dict[str, float]] = {}
|
||||
|
||||
# 설정 정규화: legacy(single) → multi 형식으로 흡수
|
||||
@@ -114,9 +113,9 @@ class ConditionSearchManager:
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._running = False
|
||||
|
||||
# 상태
|
||||
# 상태 — 순서 리스트가 본체(순차 ENTER append / EXIT remove), Set 은 조회 캐시
|
||||
self._current: Dict[str, Set[str]] = {} # strategy_id → code set
|
||||
self._current_order: Dict[str, List[str]] = {} # strategy_id → 매수 우선순위(HTS 응답 순)
|
||||
self._current_order: Dict[str, List[str]] = {} # strategy_id → push/pop 순차 목록
|
||||
self._name_map: Dict[str, str] = {} # code → name (전역)
|
||||
# 초기 tick 에서 "빈 set → 첫 결과" 를 변동으로 간주해 1회는 저장
|
||||
self._initialized: Set[str] = set()
|
||||
@@ -294,41 +293,39 @@ class ConditionSearchManager:
|
||||
|
||||
def _apply_result(self, strategy_id: str, rows: List[Dict]) -> None:
|
||||
"""
|
||||
조건검색 raw 결과를 universe 에 반영.
|
||||
조건검색 raw → 유니버스 순차 반영 (ENTER append / EXIT remove).
|
||||
|
||||
EXIT grace 정책 (``CONDITION_EXIT_GRACE_SEC`` > 0 일 때):
|
||||
- raw 결과에서 빠진 종목을 즉시 EXIT 처리하지 않고 ``_pending_exit`` 에
|
||||
``first_missing_at`` 시각과 함께 등록.
|
||||
- grace 초가 지나야 진짜 EXIT (universe 에서 제거) → WS 구독 해제 → 갭보정 리셋.
|
||||
- grace 중 다시 raw 에 등장하면 pending 에서 빼고 universe 에 그대로 keep
|
||||
(구독·캐시·봉 데이터 보존).
|
||||
|
||||
효과: 단발성 EXIT/RE-ENTER 폭주 흡수. 시장 노이즈로 1~2 tick 빠지는 케이스를
|
||||
걸러 매수 시그널 평가용 RAM 데이터 (회복률·낙폭 등) 가 휘발 안 됨.
|
||||
기본(``CONDITION_EXIT_GRACE_SEC=0``): raw 변동을 즉시 push/pop.
|
||||
grace>0: EXIT 를 N초 지연(sticky). history 는 반드시 effective 목록을
|
||||
저장해야 슬롯정합이 안 깨짐 — 부모 ``_save_snapshot`` / LS override 경로 동일.
|
||||
"""
|
||||
raw_set = {r["code"] for r in rows if r.get("code")}
|
||||
new_names = {
|
||||
r["code"]: r.get("name", r["code"]) for r in rows if r.get("code")
|
||||
}
|
||||
raw_order: List[str] = []
|
||||
raw_seen: Set[str] = set()
|
||||
new_names: Dict[str, str] = {}
|
||||
for r in rows or []:
|
||||
c = str(r.get("code") or "").strip()
|
||||
if not c or c in raw_seen:
|
||||
continue
|
||||
raw_seen.add(c)
|
||||
raw_order.append(c)
|
||||
new_names[c] = str(r.get("name") or c)
|
||||
raw_set = set(raw_seen)
|
||||
now = time.time()
|
||||
grace = self._exit_grace_sec
|
||||
|
||||
with self._lock:
|
||||
prev = self._current.get(strategy_id, set())
|
||||
prev_order = list(self._current_order.get(strategy_id) or [])
|
||||
prev = set(prev_order) if prev_order else set(self._current.get(strategy_id) or [])
|
||||
first_tick = strategy_id not in self._initialized
|
||||
pending = self._pending_exit.setdefault(strategy_id, {})
|
||||
|
||||
# ── grace 적용 ────────────────────────────────────────
|
||||
kept_in_grace: Set[str] = set()
|
||||
if grace > 0:
|
||||
# 1) raw 에 다시 등장 → grace 해제 (회생)
|
||||
for c in raw_set & set(pending.keys()):
|
||||
pending.pop(c, None)
|
||||
# 2) raw 에서 빠진 prev 종목 → pending 등록 (처음 사라진 시각)
|
||||
for c in prev - raw_set:
|
||||
if c not in pending:
|
||||
pending[c] = now
|
||||
# 3) grace 미경과 종목 → universe 에 keep / 경과 → pending 제거
|
||||
expired: Set[str] = set()
|
||||
for c, first_at in list(pending.items()):
|
||||
if now - first_at >= grace:
|
||||
@@ -336,20 +333,26 @@ class ConditionSearchManager:
|
||||
for c in expired:
|
||||
pending.pop(c, None)
|
||||
kept_in_grace = set(pending.keys())
|
||||
# grace=0 (기존 동작) 이면 kept_in_grace = ∅, pending 안 쓰임.
|
||||
else:
|
||||
pending.clear()
|
||||
|
||||
# 봇이 실제로 보는 effective universe (raw + grace keep)
|
||||
new_set = raw_set | kept_in_grace
|
||||
|
||||
# ── 변동 계산 (effective 기준) ───────────────────────
|
||||
# 순차 목록: raw 응답 순서 유지 + grace 잔여(이전 순서) + 나머지
|
||||
ordered = self._build_ordered_universe(
|
||||
rows=[{"code": c, "name": new_names.get(c, c)} for c in raw_order],
|
||||
new_set=new_set,
|
||||
prev_order=prev_order,
|
||||
)
|
||||
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 []),
|
||||
)
|
||||
# 신규 ENTER 를 맨 앞 — {SID}_CAND_LIMIT 하드캡이 오래된 저코드만
|
||||
# 남기고 AFR/스냅 신규를 잘라먹던 sticky 를 막는다.
|
||||
if enters:
|
||||
head = [c for c in ordered if c in enters]
|
||||
tail = [c for c in ordered if c not in enters]
|
||||
ordered = head + tail
|
||||
self._current_order[strategy_id] = ordered
|
||||
self._current[strategy_id] = set(ordered)
|
||||
self._initialized.add(strategy_id)
|
||||
for c, n in new_names.items():
|
||||
self._name_map[c] = n
|
||||
@@ -359,9 +362,10 @@ class ConditionSearchManager:
|
||||
|
||||
if changed:
|
||||
grace_tag = f", grace={n_grace}" if n_grace else ""
|
||||
mode_tag = "push/pop" if grace <= 0 else "grace-sticky"
|
||||
logger.info(
|
||||
"🔄 [%s] +%d / -%d (현재 %d종목%s)",
|
||||
strategy_id, len(enters), len(exits), len(new_set), grace_tag,
|
||||
"🔄 [%s] +%d / -%d (현재 %d종목%s, %s)",
|
||||
strategy_id, len(enters), len(exits), len(ordered), grace_tag, mode_tag,
|
||||
)
|
||||
if enters:
|
||||
preview = ", ".join(
|
||||
@@ -376,17 +380,15 @@ class ConditionSearchManager:
|
||||
|
||||
if self.on_change and changed:
|
||||
try:
|
||||
self.on_change(strategy_id, new_set, enters, exits)
|
||||
self.on_change(strategy_id, set(ordered), enters, exits)
|
||||
except Exception as e:
|
||||
logger.warning("on_change 콜백 예외: %s", e)
|
||||
|
||||
# 백테스트 재현성 보장: 첫 tick 또는 변동 발생 tick 마다 풀 스냅샷 저장
|
||||
# (effective universe 기준 — 봇이 실제로 보던 universe 가 그대로 기록됨)
|
||||
# 변동 없는 tick 은 공간 절약 위해 skip
|
||||
# effective 순서 그대로 history (첫 tick 또는 변동 시)
|
||||
if changed or first_tick:
|
||||
with self._lock:
|
||||
ordered = list(self._current_order.get(strategy_id) or [])
|
||||
self._save_snapshot(strategy_id, ordered, new_names)
|
||||
ordered_save = list(self._current_order.get(strategy_id) or [])
|
||||
self._save_snapshot(strategy_id, ordered_save, new_names)
|
||||
|
||||
@staticmethod
|
||||
def _build_ordered_universe(
|
||||
|
||||
Reference in New Issue
Block a user