194 lines
7.9 KiB
Python
194 lines
7.9 KiB
Python
"""
|
|
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
|