""" kis_trader/engine/us_momentum_env.py — 해외 모멘텀(US_MOMENTUM_*) env 오버레이 ============================================================================= 국내 MOMENTUM_* 와 엔진 키는 동일하되, 저장·실매는 US_MOMENTUM_* 만 사용. 값이 없으면 국내 기본(이미 get_momentum_defaults_from_db 로 로드된 params) 유지. """ from __future__ import annotations from typing import Any, Dict, Optional from kis_trader.utils.env import get_env_bool, get_env_float, get_env_from_db, get_env_int # 웹 저장·ENV_CONFIG_KEYS 등록용 (TRIGGER/청산 핵심) US_MOMENTUM_TRIGGER_EXIT_KEYS = ( "US_MOMENTUM_RSI_MIN", "US_MOMENTUM_RSI_MAX", "US_MOMENTUM_VOL_MULT", "US_MOMENTUM_VOL_WIN", "US_MOMENTUM_TIME_START", "US_MOMENTUM_TIME_END", "US_MOMENTUM_SELL_TIME_END", "US_MOMENTUM_MAX_FROM_OPEN_PCT", "US_MOMENTUM_MIN_FROM_OPEN_PCT", "US_MOMENTUM_STOP_LOSS_PCT", "US_MOMENTUM_TAKE_PROFIT_PCT", "US_MOMENTUM_TP_MAX_PCT", "US_MOMENTUM_SHOULDER_MIN_HIGH_PCT", "US_MOMENTUM_SHOULDER_CUT_PCT", "US_MOMENTUM_RATCHET_TIERS", "US_MOMENTUM_TRAIL_PCT", "US_MOMENTUM_TRAIL_ARM_PCT", "US_MOMENTUM_MAX_HOLD_BARS", "US_MOMENTUM_MAX_DAILY", "US_MOMENTUM_MIN_PRICE", "US_MOMENTUM_MAX_DAILY_CHG", "US_MOMENTUM_HIGH_CHASE_THR", "US_MOMENTUM_USE_DEFENSE_FILTERS", "US_MOMENTUM_USE_HIGH_CHASE_FILTER", "US_MOMENTUM_USE_DAILY_RANGE_FILTER", "US_MOMENTUM_USE_EMA_FILTER", "US_MOMENTUM_EMA_FAST_PERIOD", "US_MOMENTUM_EMA_SLOW_PERIOD", "US_MOMENTUM_USE_RSI_MAX_FILTER", "US_MOMENTUM_PATTERN_BREAKOUT", "US_MOMENTUM_PATTERN_PULLBACK", "US_MOMENTUM_CHASE_LOOKBACK_MIN", "US_MOMENTUM_PULLBACK_LOOKBACK_MIN", "US_MOMENTUM_PULLBACK_MIN_PCT", "US_MOMENTUM_PULLBACK_MAX_PCT", "US_MOMENTUM_SETUP_VOL_MAX_MULT", "US_MOMENTUM_SETUP_BEAR_BARS_MIN", "US_MOMENTUM_MAX_LOSS_PER_TRADE", "US_MOMENTUM_MIN_PROFIT_PCT", "US_MOMENTUM_SLOT_MONEY", "US_MOMENTUM_MAX_STOCKS", "US_MOMENTUM_TOTAL_BUDGET", "US_MOMENTUM_MAX_BUY_AMOUNT", "US_MOMENTUM_COOLDOWN_SEC", "US_MOMENTUM_EOD_ENABLED", "US_MOMENTUM_EOD_HM", "US_MOMENTUM_DAILY_PROFIT_TARGET_ENABLED", "US_MOMENTUM_DAILY_PROFIT_TRAIL_TIERS", "US_MOMENTUM_DAILY_PROFIT_MODE", ) def _pct_ratio_from_env(key: str) -> Optional[float]: """DB 소수(0.015) 또는 퍼센트(1.5) → 엔진 비율. 키 없으면 None.""" raw = get_env_from_db(key, "") if raw in (None, "", "None"): return None try: x = abs(float(raw)) except (TypeError, ValueError): return None if x == 0: return 0.0 return x if x < 0.5 else x / 100.0 def overlay_us_momentum_engine_params(params: Dict[str, Any]) -> Dict[str, Any]: """국내 defaults 위에 US_MOMENTUM_* 가 있으면 덮어씀 (실매 UsMomentumStrategy).""" out = dict(params or {}) def set_if(key: str, env_key: str, cast=float): raw = get_env_from_db(env_key, "") if raw in (None, "", "None"): return try: out[key] = cast(raw) except (TypeError, ValueError): pass for eng, envk, cast in ( ("mom_rsi_min", "US_MOMENTUM_RSI_MIN", float), ("mom_rsi_max", "US_MOMENTUM_RSI_MAX", float), ("mom_vol_mult", "US_MOMENTUM_VOL_MULT", float), ("mom_vol_win", "US_MOMENTUM_VOL_WIN", int), ("max_daily", "US_MOMENTUM_MAX_DAILY", int), ("min_price", "US_MOMENTUM_MIN_PRICE", float), ("max_daily_chg", "US_MOMENTUM_MAX_DAILY_CHG", float), ("high_chase_thr", "US_MOMENTUM_HIGH_CHASE_THR", float), ("mom_max_from_open_pct", "US_MOMENTUM_MAX_FROM_OPEN_PCT", float), ("mom_min_from_open_pct", "US_MOMENTUM_MIN_FROM_OPEN_PCT", float), ("chase_lookback_min", "US_MOMENTUM_CHASE_LOOKBACK_MIN", int), ("pullback_lookback_min", "US_MOMENTUM_PULLBACK_LOOKBACK_MIN", int), ("pullback_min_pct", "US_MOMENTUM_PULLBACK_MIN_PCT", float), ("pullback_max_pct", "US_MOMENTUM_PULLBACK_MAX_PCT", float), ("setup_vol_max_mult", "US_MOMENTUM_SETUP_VOL_MAX_MULT", float), ("setup_bear_bars_min", "US_MOMENTUM_SETUP_BEAR_BARS_MIN", int), ("ema_fast_period", "US_MOMENTUM_EMA_FAST_PERIOD", int), ("ema_slow_period", "US_MOMENTUM_EMA_SLOW_PERIOD", int), ("max_hold_bars", "US_MOMENTUM_MAX_HOLD_BARS", int), ("slot_money", "US_MOMENTUM_SLOT_MONEY", int), ("max_stocks", "US_MOMENTUM_MAX_STOCKS", int), ): set_if(eng, envk, cast) # 세션 HM 은 국내 defaults(900~1530)가 남아 있으면 야간 US 봉이 조용히 스킵됨. # US_MOMENTUM_TIME_* 는 항상 강제 (DB 없으면 코드 기본 2230~500). _ts = int(get_env_int("US_MOMENTUM_TIME_START", 2230) or 2230) _te = int(get_env_int("US_MOMENTUM_TIME_END", 500) or 500) out["time_start_hm"] = _ts out["time_start"] = _ts out["time_end_hm"] = _te out["mom_time_end_hm"] = _te out["mom_time_end"] = _te for eng, envk in ( ("sl_pct", "US_MOMENTUM_STOP_LOSS_PCT"), ("tp_pct", "US_MOMENTUM_TAKE_PROFIT_PCT"), ("tp_max_pct", "US_MOMENTUM_TP_MAX_PCT"), ("shoulder_min_high", "US_MOMENTUM_SHOULDER_MIN_HIGH_PCT"), ("shoulder_cut_pct", "US_MOMENTUM_SHOULDER_CUT_PCT"), ("trail_pct", "US_MOMENTUM_TRAIL_PCT"), ("trail_arm_pct", "US_MOMENTUM_TRAIL_ARM_PCT"), ): r = _pct_ratio_from_env(envk) if r is not None: out[eng] = r cd = get_env_int("US_MOMENTUM_COOLDOWN_SEC", 0) if cd > 0: out["cooldown_min"] = cd / 60.0 rt = get_env_from_db("US_MOMENTUM_RATCHET_TIERS", "") if rt not in (None, "", "None"): out["ratchet_tiers"] = str(rt).strip() mp = get_env_float("US_MOMENTUM_MIN_PROFIT_PCT", -1.0) if mp >= 0: # UI는 % 숫자(0.2)로 저장하는 경우 많음 → 엔진은 그대로 쓰거나 소수 out["min_margin"] = mp / 100.0 if mp >= 0.5 else mp for eng, envk, default in ( ("use_defense_filters", "US_MOMENTUM_USE_DEFENSE_FILTERS", True), ("use_high_chase_filter", "US_MOMENTUM_USE_HIGH_CHASE_FILTER", False), ("use_daily_range_filter", "US_MOMENTUM_USE_DAILY_RANGE_FILTER", False), ("use_ema_filter", "US_MOMENTUM_USE_EMA_FILTER", True), ("use_rsi_max_filter", "US_MOMENTUM_USE_RSI_MAX_FILTER", False), ("pattern_breakout", "US_MOMENTUM_PATTERN_BREAKOUT", True), ("pattern_pullback", "US_MOMENTUM_PATTERN_PULLBACK", True), ): raw = get_env_from_db(envk, "") if raw not in (None, "", "None"): out[eng] = get_env_bool(envk, default) # 해외 고정 out["_orderbook_filter_enabled"] = False out["_program_filter_enabled"] = False # HTS SCAN 없음 — skip_hts 숏컷(무조건 시그널) 금지. 추격 패턴은 hts_logic US 분기. out["skip_hts_scan_dupes"] = True out["trigger_e_confirm"] = False out["use_rsi_filter"] = True # V4: RSI 하한 적용 (US 추격 경로) out["market"] = "US" out["_session_wrap_midnight"] = True # 국내 min_price(천원대) 잔존 시 USD 종목 전부 탈락-최소가격 out["min_price"] = float(get_env_float("US_MOMENTUM_MIN_PRICE", 1.0) or 1.0) # 고정 유니버스 — 손절액으로 슬롯 재계산하지 않음 (DB SLOT_MONEY 고정) out["max_loss_krw"] = 0.0 # EOD: 국내 MOMENTUM_EOD_HM(15:20) 잔존 금지 — US 전용만 out["eod_enabled"] = bool(get_env_bool("US_MOMENTUM_EOD_ENABLED", False)) _eod = str(get_env_from_db("US_MOMENTUM_EOD_HM", "05:00") or "05:00").strip() or "05:00" # 잘못 저장된 국장대(10:00~16:59)면 해외 기본 05:00 으로 try: _eh = int(_eod.replace(":", "")[:4]) if ":" in _eod else int(float(_eod)) except (TypeError, ValueError): _eh = 500 if 1000 <= _eh <= 1659: _eod = "05:00" out["eod_hm"] = _eod out["force_eod_exit"] = bool(out["eod_enabled"]) return out