#!/usr/bin/env python3 """ env 타임라인 — 백테스트 시 env_config / config_* 변경 시각별 스냅샷 재생. 실매·파람서치는 최신 env 1행만 사용. 웹 백테만 ``EnvTimeline`` 으로 봉 시각(candle_time)마다 그 시점까지의 DB 설정을 적용한다. """ from __future__ import annotations from bisect import bisect_right from typing import Any, Dict, List, Optional, Tuple def _created_at_to_key(created_at: str) -> str: """``YYYY-MM-DD HH:MM:SS`` → ``YYYYMMDDHHMMSS``.""" s = str(created_at or "").strip() if len(s) >= 19: return ( s[0:4] + s[5:7] + s[8:10] + s[11:13] + s[14:16] + s[17:19] ) return "" def _candle_time_to_lookup_key(candle_time: str) -> str: """분봉 키(YYYYMMDDHHMM) → 해당 분 마감초(…59) lookup 키.""" t = str(candle_time or "")[:12] if len(t) < 12: return "" return t + "59" def _ymd_to_start_bound(ymd: str) -> str: y = str(ymd or "")[:8] if len(y) < 8: return "" return f"{y[:4]}-{y[4:6]}-{y[6:8]} 00:00:00" class EnvTimeline: """env 변경 이벤트 시계열 — ``snapshot_at`` 으로 봉 시각별 merged env 조회.""" def __init__(self, events: List[Tuple[str, Dict[str, Any]]]) -> None: deduped: List[Tuple[str, Dict[str, Any]]] = [] prev_sig: Optional[Tuple[str, ...]] = None for key, snap in events: if not key: continue sig = tuple(sorted((k, str(v)) for k, v in snap.items())) if sig == prev_sig: continue prev_sig = sig deduped.append((key, dict(snap))) self._keys: List[str] = [e[0] for e in deduped] self._snaps: List[Dict[str, Any]] = [e[1] for e in deduped] @property def snapshot_count(self) -> int: return len(self._keys) def snapshot_at(self, candle_time: str) -> Tuple[Dict[str, Any], str]: """봉 시각 기준 적용 env 스냅샷 + 이벤트 키(YYYYMMDDHHMMSS).""" if not self._keys: return {}, "" qkey = _candle_time_to_lookup_key(candle_time) if not qkey: return dict(self._snaps[0]), self._keys[0] idx = bisect_right(self._keys, qkey) - 1 if idx < 0: return {}, "" return dict(self._snaps[idx]), self._keys[idx] def build_env_timeline( db: Any, start_ymd: str, end_ymd: str, ) -> Optional[EnvTimeline]: """백테 구간 env 변경 이력 → ``EnvTimeline`` (변경 없으면 1스냅샷).""" if db is None: return None sy = str(start_ymd or "")[:8] ey = str(end_ymd or "")[:8] if len(sy) < 8 or len(ey) < 8: return None if not hasattr(db, "list_env_change_times_in_range"): return None if not hasattr(db, "get_merged_env_snapshot_as_of"): return None start_bound = _ymd_to_start_bound(sy) event_times = db.list_env_change_times_in_range(sy, ey) if not event_times: event_times = [start_bound] events: List[Tuple[str, Dict[str, Any]]] = [] for as_of in event_times: merged = db.get_merged_env_snapshot_as_of(as_of) key = _created_at_to_key(as_of) if not key: continue events.append((key, merged)) if not events: return None return EnvTimeline(events)