#!/usr/bin/env python3 """ 모멘텀 백테 유니버스 — 스캔 시각(초)별 ``get_universe_at`` 재현 + EXIT 디바운스. 실매: 조건검색 변동 시 초단위 스냅샷 → 10초 루프에서 그 시각 최신 유니버스. 백테(기존): 1분 슬롯 strict 집계 → 깜빡임 EXIT·분봉 끝 집계로 실매와 어긋남. ※ 초단위 타임라인 로직은 전략 공통 ``universe_timeline.UniverseTimeline`` 으로 통일했다. 이 모듈은 모멘텀 전용 wrapper(debounce/scan_at env·attach)만 유지하며, ``MomentumUniverseTimeline`` 은 공통 클래스의 alias 다. (기존 import 무변경) """ from __future__ import annotations from typing import Any, Dict, Optional from kis_trader.engine.momentum_engine import MOMENTUM_STRATEGY_ID # 전략 공통 타임라인 (단일 구현) — 기존 이름 재노출/alias 로 모멘텀 코드 무변경 유지. from kis_trader.backtest.universe_timeline import ( # noqa: F401 UniverseTimeline, build_universe_timeline, debounce_universe_snapshots, normalize_universe_events, ) # 하위호환 alias — 모멘텀 코드/외부 import 가 계속 쓰는 이름. MomentumUniverseTimeline = UniverseTimeline def momentum_universe_exit_debounce_sec(params: Optional[Dict[str, Any]] = None) -> int: """조건검색 짧은 EXIT→재편입 깜빡임 무시(초). grace 스냅샷 재생 시 기본 0.""" if params is not None and params.get("universe_exit_debounce_sec") is not None: try: return max(0, int(float(params["universe_exit_debounce_sec"]))) except (TypeError, ValueError): pass from kis_trader.backtest.momentum_backtest_common import ( momentum_universe_exit_debounce_sec as _deb_from_common, ) return _deb_from_common() def momentum_backtest_universe_scan_at_enabled(params: Optional[Dict[str, Any]] = None) -> bool: """백테 스캔 루프: 분 슬롯 대신 스캔 시각 유니버스 (기본 ON).""" if params is not None and params.get("backtest_universe_scan_at") is not None: s = str(params.get("backtest_universe_scan_at")).strip().lower() if s in ("1", "true", "t", "y", "yes", "on"): return True if s in ("0", "false", "f", "n", "no", "off", ""): return False from kis_trader.utils.env import get_env_bool return get_env_bool("MOMENTUM_BACKTEST_UNIVERSE_SCAN_AT", True) def build_momentum_universe_timeline( *, strategy_id: str, start_ymd: str, end_ymd: str, debounce_sec: int = 30, strict: bool = False, strict_lag_minutes: int = 1, history_source: str = "kiwoom", ) -> Optional[UniverseTimeline]: """모멘텀 유니버스 타임라인 — 전략 공통 ``build_universe_timeline`` 사용.""" return build_universe_timeline( strategy_id=strategy_id, start_ymd=start_ymd, end_ymd=end_ymd, debounce_sec=debounce_sec, strict=strict, strict_lag_minutes=strict_lag_minutes, history_source=history_source, ) def attach_momentum_universe_timeline_to_params( params: Dict[str, Any], *, start_ymd: str, end_ymd: str, strategy_id: str = MOMENTUM_STRATEGY_ID, use_saved_history: bool = True, history_source: Optional[str] = None, ) -> Optional[UniverseTimeline]: """백테 params에 ``_momentum_universe_timeline`` 부착 (스캔 시각 유니버스).""" if not use_saved_history: return None if not momentum_backtest_universe_scan_at_enabled(params): return None from kis_trader.backtest.universe_history_source import ( resolve_backtest_universe_history_source, ) hs = resolve_backtest_universe_history_source( history_source if history_source is not None and str(history_source).strip() != "" else ( params.get("_universe_history_source") or params.get("universe_history_source") ) ) params["_universe_history_source"] = hs debounce_sec = momentum_universe_exit_debounce_sec(params) # scan_at = 실매 get_universe_at(초) — 분봉 strict lag 는 minute 슬롯 전용 timeline = build_momentum_universe_timeline( strategy_id=strategy_id, start_ymd=start_ymd, end_ymd=end_ymd, debounce_sec=debounce_sec, strict=False, strict_lag_minutes=0, history_source=hs, ) if timeline is not None: params["_momentum_universe_timeline"] = timeline params["_universe_timeline_meta"] = { "debounce_sec": debounce_sec, "strict": False, "strict_lag_minutes": 0, "snapshots": timeline.snapshot_count, "mode": "scan_at", "history_source": hs, } return timeline