""" ws_candles 시리즈 — source=증권사, channel=ws|rest|rollup. 저장은 섞지 않음: 키움 REST 갭보정은 항상 source=kiwoom, channel=rest. 읽기(실매=Optuna): 틱 메인 WS 우선, 구멍만 kiwoom+rest. """ from __future__ import annotations import os from typing import Any, Dict, List, Optional, Sequence, Tuple from kis_trader.utils.env import get_env_from_db CHANNEL_WS = "ws" CHANNEL_REST = "rest" CHANNEL_ROLLUP = "rollup" # 읽기 우선순위 쌍 (source, channel) ReadPair = Tuple[str, str] def normalize_source_channel( source: str = "", channel: str = "", ) -> Tuple[str, str]: """레거시 source=rest|kw_rest|rollup_1m 을 (증권사, channel)로 정규화.""" s = str(source or "").strip().lower() ch = str(channel or "").strip().lower() if s in ("rest", "kw_rest"): return "kiwoom", CHANNEL_REST if s in ("rollup_1m", "rollup"): src = "kiwoom" if ch in ("kis",): src = "kis" return src, CHANNEL_ROLLUP if s in ("kis", "kis_us", "ws", ""): return "kis", (ch or CHANNEL_WS) if s == "kiwoom": return "kiwoom", (ch or CHANNEL_WS) if s == "ls": return "ls", (ch or CHANNEL_WS) return (s or "kis")[:16], (ch or CHANNEL_WS)[:10] def _provider_and_override() -> Tuple[str, str]: override = "" raw = os.environ.get("CANDLE_SOURCE") if raw is None: try: raw = get_env_from_db("CANDLE_SOURCE", "") except Exception: raw = "" override = str(raw or "").strip().lower() if override not in ("kis", "kiwoom"): override = "" try: provider = ( get_env_from_db("LIVE_TICK_PROVIDER", "kiwoom") or "kiwoom" ).strip().lower() except Exception: provider = "kiwoom" if provider not in ("kis", "kiwoom"): provider = "kiwoom" return provider, override def live_read_pairs() -> Tuple[ReadPair, ...]: """실매·Optuna 기본 읽기 순서. 틱 메인 WS 한 칸, 없으면 키움 REST 구멍. CANDLE_SOURCE=kis|kiwoom 이면 그 메인을 씀. 빈값이면 LIVE_TICK_PROVIDER. """ provider, override = _provider_and_override() main = override or provider # 메인 WS → 구멍 키움 REST → 1M 롤업(상위 TF 웜업). 증권사 행은 섞지 않음. if main == "kis": return ( ("kis", CHANNEL_WS), ("kiwoom", CHANNEL_REST), ("kiwoom", CHANNEL_ROLLUP), ) return ( ("kiwoom", CHANNEL_WS), ("kiwoom", CHANNEL_REST), ("kiwoom", CHANNEL_ROLLUP), ) def live_read_label() -> str: provider, override = _provider_and_override() main = override or provider if override: return f"{override.upper()}+kiwoom_rest" return f"LIVE({main})+kiwoom_rest" def dedupe_by_read_pairs( rows: Sequence[Dict[str, Any]], pairs: Optional[Sequence[ReadPair]] = None, ) -> List[Dict[str, Any]]: """candle_time 1행 — pairs 앞쪽이 이김. source/channel 은 결과에 남기지 않음(레거시 호환).""" order = tuple(pairs or live_read_pairs()) rank = {p: i for i, p in enumerate(order)} seen: Dict[str, Tuple[int, Dict[str, Any]]] = {} for row in rows: ct = str(row.get("candle_time") or "")[:12] if not ct: continue src, ch = normalize_source_channel( str(row.get("source") or ""), str(row.get("channel") or ""), ) pri = rank.get((src, ch), 999) prev = seen.get(ct) if prev is None or pri < prev[0]: seen[ct] = (pri, dict(row)) out: List[Dict[str, Any]] = [] for ct in sorted(seen.keys()): d = seen[ct][1] d.pop("source", None) d.pop("channel", None) out.append(d) return out