""" kis_trader/engine/orderbook_env.py — 호가필터 env 키 (전략 전용) ================================================================ 글로벌 ``ORDERBOOK_*`` 임계값은 폐기. 실매·그리드·apply-best 는 ``{SCALP|TAIL|MOMENTUM|BREAKOUT}_ORDERBOOK_*`` 만 사용. 코드 기본값 = DB 시드·웹 스키마 default 와 동일해야 함. """ from __future__ import annotations from typing import Any, Dict, List, Optional, Tuple from kis_trader.utils.env import get_env_bool, get_env_float, get_env_from_db, get_env_int # 임계값 기본 (하드코딩 금지 규칙: get_env_* 기본 인자로만 사용 · DB 시드와 동기) OB_DEFAULT_FILTER_ENABLED = False # 현행 실매 OFF 유지 OB_DEFAULT_MAX_SPREAD_PCT = 0.45 OB_DEFAULT_ENTRY_BID_LEVELS = 3 OB_DEFAULT_ENTRY_BID_DEPTH_MULT = 1.2 OB_DEFAULT_ENTRY_ASK_MAX_MULT = 3.0 OB_DEFAULT_MIN_BID_ASK_RATIO = 0.85 OB_DEFAULT_ASK_WALL_MAX_QTY = 5000 # 시드·UI 대상 전략 prefix (SHORT 오케스트레이터 → TAIL_* canonical) OB_STRATEGY_PREFIXES: Tuple[str, ...] = ("SCALP", "TAIL", "MOMENTUM", "BREAKOUT") def orderbook_strategy_prefix(strategy: str) -> str: s = (strategy or "").strip().upper() if s in ("BREAKOUT", "BO"): return "BREAKOUT" if s in ("MOMENTUM", "MOM"): return "MOMENTUM" if s in ("TAIL", "SHORT", "TAIL_CATCH"): return "TAIL" if s in ("SCALP", "SCALPING", "REVERSAL"): return "SCALP" return s def _key(pfx: str, suffix: str) -> str: return f"{pfx}_ORDERBOOK_{suffix}" def orderbook_filter_enabled(strategy: str) -> bool: """전략별 ON/OFF. 글로벌 ORDERBOOK_FILTER_ENABLED 폐기 → 기본 OFF.""" pfx = orderbook_strategy_prefix(strategy) if not pfx or pfx == "ORDERBOOK": return False return get_env_bool(_key(pfx, "FILTER_ENABLED"), OB_DEFAULT_FILTER_ENABLED) def get_ob_float(strategy: str, suffix: str, default: float) -> float: pfx = orderbook_strategy_prefix(strategy) if not pfx: return float(default) return float(get_env_float(_key(pfx, suffix), default)) def get_ob_int(strategy: str, suffix: str, default: int) -> int: pfx = orderbook_strategy_prefix(strategy) if not pfx: return int(default) return int(get_env_int(_key(pfx, suffix), default)) def load_orderbook_threshold_cfg( strategy: str, params: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """실매/백테 공통 임계값. params 의 ``_ob_*`` 가 있으면 덮어씀(파람서치).""" pfx = orderbook_strategy_prefix(strategy) cfg = { "max_spread_pct": get_ob_float(pfx, "MAX_SPREAD_PCT", OB_DEFAULT_MAX_SPREAD_PCT), "entry_bid_levels": get_ob_int(pfx, "ENTRY_BID_LEVELS", OB_DEFAULT_ENTRY_BID_LEVELS), "entry_bid_depth_mult": get_ob_float( pfx, "ENTRY_BID_DEPTH_MULT", OB_DEFAULT_ENTRY_BID_DEPTH_MULT, ), "entry_ask_max_mult": get_ob_float( pfx, "ENTRY_ASK_MAX_MULT", OB_DEFAULT_ENTRY_ASK_MAX_MULT, ), "min_bid_ask_ratio": get_ob_float( pfx, "MIN_BID_ASK_RATIO", OB_DEFAULT_MIN_BID_ASK_RATIO, ), "breakout_ask_wall_max_qty": get_ob_int( pfx, "ASK_WALL_MAX_QTY", OB_DEFAULT_ASK_WALL_MAX_QTY, ), } if params: _overrides = ( ("_ob_max_spread_pct", "max_spread_pct"), ("_ob_min_bid_ask_ratio", "min_bid_ask_ratio"), ("_ob_ask_max_mult", "entry_ask_max_mult"), ("_ob_ask_wall_max_qty", "breakout_ask_wall_max_qty"), ("_ob_bid_depth_mult", "entry_bid_depth_mult"), ("_ob_bid_levels", "entry_bid_levels"), ) for src_key, cfg_key in _overrides: ov = params.get(src_key) if ov is not None and str(ov).strip() != "": try: cfg[cfg_key] = float(ov) except (TypeError, ValueError): pass return cfg def orderbook_params_to_env_patch(strategy: str, params: Dict[str, Any]) -> Dict[str, str]: """그리드/Optuna merged → 전략별 ORDERBOOK_* 패치 (값 있을 때만).""" pfx = orderbook_strategy_prefix(strategy) if not pfx: return {} out: Dict[str, str] = {} if "max_spread_pct" in params and params.get("max_spread_pct") not in (None, ""): out[_key(pfx, "MAX_SPREAD_PCT")] = str(float(params["max_spread_pct"])) if "min_bid_ask_ratio" in params and params.get("min_bid_ask_ratio") not in (None, ""): out[_key(pfx, "MIN_BID_ASK_RATIO")] = str(float(params["min_bid_ask_ratio"])) if "ask_max_mult" in params and params.get("ask_max_mult") not in (None, ""): out[_key(pfx, "ENTRY_ASK_MAX_MULT")] = str(float(params["ask_max_mult"])) if "ask_wall_max_qty" in params and params.get("ask_wall_max_qty") not in (None, ""): out[_key(pfx, "ASK_WALL_MAX_QTY")] = str(int(float(params["ask_wall_max_qty"]))) if "bid_depth_mult" in params and params.get("bid_depth_mult") not in (None, ""): out[_key(pfx, "ENTRY_BID_DEPTH_MULT")] = str(float(params["bid_depth_mult"])) if "bid_levels" in params and params.get("bid_levels") not in (None, ""): out[_key(pfx, "ENTRY_BID_LEVELS")] = str(int(float(params["bid_levels"]))) # 호가축 apply → FILTER_ENABLED (_orderbook_filter_enabled / ob_filter_enabled) fe = params.get("ob_filter_enabled") if fe is None: fe = params.get("_orderbook_filter_enabled") if fe is not None and str(fe).strip() != "": out[_key(pfx, "FILTER_ENABLED")] = ( "true" if str(fe).lower() in ("1", "true", "yes", "on") else "false" ) return out def build_orderbook_seed_patch( *, filter_enabled: Optional[bool] = None, copy_global_if_empty: bool = True, ) -> Dict[str, str]: """전략별 기본값 시드. 빈 칸만 채움(이미 값 있으면 유지). copy_global_if_empty: 구 글로벌 ORDERBOOK_* 가 있으면 1회 이관. """ fe = OB_DEFAULT_FILTER_ENABLED if filter_enabled is None else bool(filter_enabled) # 글로벌 → 전략 이관용 (폐기 전 잔존값) g_spread = str(get_env_from_db("ORDERBOOK_MAX_SPREAD_PCT", "") or "").strip() g_levels = str(get_env_from_db("ORDERBOOK_ENTRY_BID_LEVELS", "") or "").strip() g_depth = str(get_env_from_db("ORDERBOOK_ENTRY_BID_DEPTH_MULT", "") or "").strip() g_ask = str(get_env_from_db("ORDERBOOK_ENTRY_ASK_MAX_MULT", "") or "").strip() g_ratio = str(get_env_from_db("ORDERBOOK_MIN_BID_ASK_RATIO", "") or "").strip() g_wall = str(get_env_from_db("ORDERBOOK_BREAKOUT_ASK_WALL_MAX_QTY", "") or "").strip() g_filt = str(get_env_from_db("ORDERBOOK_FILTER_ENABLED", "") or "").strip() patch: Dict[str, str] = {} for pfx in OB_STRATEGY_PREFIXES: pairs: List[Tuple[str, str, str]] = [ ("FILTER_ENABLED", "false" if not fe else "true", g_filt), ("MAX_SPREAD_PCT", str(OB_DEFAULT_MAX_SPREAD_PCT), g_spread), ("ENTRY_BID_LEVELS", str(OB_DEFAULT_ENTRY_BID_LEVELS), g_levels), ("ENTRY_BID_DEPTH_MULT", str(OB_DEFAULT_ENTRY_BID_DEPTH_MULT), g_depth), ("ENTRY_ASK_MAX_MULT", str(OB_DEFAULT_ENTRY_ASK_MAX_MULT), g_ask), ("MIN_BID_ASK_RATIO", str(OB_DEFAULT_MIN_BID_ASK_RATIO), g_ratio), ] if pfx == "BREAKOUT": pairs.append(("ASK_WALL_MAX_QTY", str(OB_DEFAULT_ASK_WALL_MAX_QTY), g_wall)) for suf, default_s, glob_s in pairs: k = _key(pfx, suf) cur = str(get_env_from_db(k, "") or "").strip() if cur: continue if suf == "FILTER_ENABLED": # 시드 우선순위: 호출자 fe → (이관 시) 글로벌 → 코드 기본 if filter_enabled is not None: patch[k] = "true" if fe else "false" elif copy_global_if_empty and glob_s: patch[k] = ( "true" if glob_s.lower() in ("1", "true", "yes", "on") else "false" ) else: patch[k] = default_s elif copy_global_if_empty and glob_s: patch[k] = glob_s else: patch[k] = default_s return patch