ls증권 히스토리 구독 넣음

This commit is contained in:
Your Name
2026-07-30 18:05:07 +09:00
parent 61bec4bd1d
commit 67eab24603
1593 changed files with 135733 additions and 1232 deletions

View File

@@ -0,0 +1,296 @@
"""
kis_trader/engine/us_momentum_env_keys.py — 해외 모멘텀(US_MOMENTUM) env 키
==========================================================================
config_us_momentum · UsMomentumStrategy · 웹 · Optuna apply 공통.
국내 MOMENTUM_* / config_momentum 과 분리 (오염 금지).
"""
from __future__ import annotations
from typing import Any, Dict, Optional
# ENV_CONFIG_KEYS / classify → config_us_momentum 과 동기화
US_MOMENTUM_CONFIG_KEYS = frozenset({
"STRATEGY_US_MOMENTUM_ENABLED",
"US_MOMENTUM_TIME_START",
"US_MOMENTUM_TIME_END",
"US_MOMENTUM_SESSION_START",
"US_MOMENTUM_BUY_WAIT_LOG_SEC",
"US_MOMENTUM_SELL_TIME_END",
"US_MOMENTUM_DRY_RUN",
"US_MOMENTUM_MAX_STOCKS",
"US_MOMENTUM_SLOT_MONEY",
"US_MOMENTUM_FEE_RATE",
"US_MOMENTUM_SELL_TAX",
"US_MOMENTUM_FX_FEE_RATE",
"US_MOMENTUM_MIN_PRICE",
"US_MOMENTUM_LIVE_MIN_CANDLES",
"US_MOMENTUM_EOD_ENABLED",
"US_MOMENTUM_EOD_HM",
"US_MOMENTUM_MAX_BUY_AMOUNT",
"US_MOMENTUM_TOTAL_BUDGET",
"US_MOMENTUM_MAX_ORDER_QTY",
"US_MOMENTUM_FORCE_BUY_QTY",
"US_MOMENTUM_RELOAD_INTERVAL_SEC",
"US_MOMENTUM_PSAMOUNT_CACHE_SEC",
"US_MOMENTUM_TRADES_CACHE_SEC",
"US_MOMENTUM_LOOP_SLEEP_MIN",
"US_MOMENTUM_LOOP_SLEEP_MAX",
"US_MOMENTUM_SCAN_REJECT_SLEEP_MIN",
"US_MOMENTUM_SCAN_REJECT_SLEEP_MAX",
"US_MOMENTUM_RSI_MIN",
"US_MOMENTUM_RSI_MAX",
"US_MOMENTUM_VOL_MULT",
"US_MOMENTUM_VOL_WIN",
"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_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_COOLDOWN_SEC",
"US_MOMENTUM_DAILY_PROFIT_TARGET_ENABLED",
"US_MOMENTUM_DAILY_PROFIT_TRAIL_TIERS",
"US_MOMENTUM_DAILY_PROFIT_MODE",
"KIS_US_MOMENTUM_MM_CHANNEL",
})
def _env_bool_10(v: Any) -> str:
if v is True or str(v).strip().lower() in ("1", "true", "t", "y", "yes", "on"):
return "1"
return "0"
def params_to_us_momentum_env_patch(p: Dict[str, Any]) -> Dict[str, str]:
"""파라서치·웹 merged(UI %) → US_MOMENTUM_* 패치 (호가/프로그램 제외)."""
if not p:
return {}
patch: Dict[str, str] = {}
def gi(key: str, default: int = 0) -> int:
v = p.get(key)
return int(float(v)) if v not in (None, "") else default
def gf(key: str, default: float = 0.0) -> float:
v = p.get(key)
return float(v) if v not in (None, "") else default
def _set(k: str, v: Any) -> None:
if v is not None and v != "":
patch[k] = str(v)
if "mom_rsi_min" in p:
_set("US_MOMENTUM_RSI_MIN", gi("mom_rsi_min", 50))
if "mom_rsi_max" in p:
_set("US_MOMENTUM_RSI_MAX", gi("mom_rsi_max", 80))
if "mom_vol_mult" in p:
_set("US_MOMENTUM_VOL_MULT", gf("mom_vol_mult", 1.5))
if "mom_vol_win" in p:
_set("US_MOMENTUM_VOL_WIN", gi("mom_vol_win", 5))
# TIME_* — session_env_patch 기본 OFF. 여기선 명시 키만(웹 저장용).
if "time_start" in p or "time_start_hm" in p:
ts = p.get("time_start_hm", p.get("time_start"))
if ts not in (None, ""):
_set("US_MOMENTUM_TIME_START", int(float(ts)))
if "time_end" in p or "time_end_hm" in p or "mom_time_end_hm" in p:
te = p.get("mom_time_end_hm", p.get("time_end_hm", p.get("time_end")))
if te not in (None, ""):
_set("US_MOMENTUM_TIME_END", int(float(te)))
if "sell_time_end" in p and p.get("sell_time_end") not in (None, ""):
_set("US_MOMENTUM_SELL_TIME_END", int(float(p["sell_time_end"])))
if "sl_pct" in p:
_set("US_MOMENTUM_STOP_LOSS_PCT", abs(gf("sl_pct", 1.5)) / 100.0)
if "tp_pct" in p:
_set("US_MOMENTUM_TAKE_PROFIT_PCT", abs(gf("tp_pct", 2.5)) / 100.0)
if "tp_max_pct" in p and p.get("tp_max_pct") not in (None, ""):
_set("US_MOMENTUM_TP_MAX_PCT", abs(float(p["tp_max_pct"])) / 100.0)
if "cooldown_min" in p and p.get("cooldown_min") is not None:
_set("US_MOMENTUM_COOLDOWN_SEC", int(float(p["cooldown_min"]) * 60))
if "max_daily" in p:
_set("US_MOMENTUM_MAX_DAILY", gi("max_daily", 5))
sm = p.get("slot_money")
if sm not in (None, ""):
sms = str(int(float(sm)))
patch["US_MOMENTUM_SLOT_MONEY"] = sms
patch["US_MOMENTUM_MAX_BUY_AMOUNT"] = sms
if "slots" in p or "max_stocks" in p:
ms = p.get("max_stocks", p.get("slots"))
if ms not in (None, ""):
_set("US_MOMENTUM_MAX_STOCKS", int(float(ms)))
if "total_budget_krw" in p and p.get("total_budget_krw") not in (None, ""):
_set("US_MOMENTUM_TOTAL_BUDGET", int(float(p["total_budget_krw"])))
if "high_chase_thr" in p and p.get("high_chase_thr") is not None:
hc = float(p["high_chase_thr"])
_set("US_MOMENTUM_HIGH_CHASE_THR", hc if 0 < hc <= 1 else hc / 100.0)
if "max_daily_chg" in p:
_set("US_MOMENTUM_MAX_DAILY_CHG", gf("max_daily_chg", 20.0))
if "min_price" in p:
_set("US_MOMENTUM_MIN_PRICE", gf("min_price", 1.0))
if "max_loss_krw" in p and p.get("max_loss_krw") not in (None, ""):
_set("US_MOMENTUM_MAX_LOSS_PER_TRADE", int(float(p["max_loss_krw"])))
if "min_margin" in p:
_set("US_MOMENTUM_MIN_PROFIT_PCT", gf("min_margin", 0.2))
if "mom_max_from_open_pct" in p and p.get("mom_max_from_open_pct") not in (None, ""):
_set("US_MOMENTUM_MAX_FROM_OPEN_PCT", float(p["mom_max_from_open_pct"]))
if "mom_min_from_open_pct" in p and p.get("mom_min_from_open_pct") not in (None, ""):
_set("US_MOMENTUM_MIN_FROM_OPEN_PCT", float(p["mom_min_from_open_pct"]))
if "shoulder_min_high" in p and p.get("shoulder_min_high") not in (None, ""):
_set("US_MOMENTUM_SHOULDER_MIN_HIGH_PCT", abs(float(p["shoulder_min_high"])) / 100.0)
if "shoulder_cut_pct" in p and p.get("shoulder_cut_pct") not in (None, ""):
_set("US_MOMENTUM_SHOULDER_CUT_PCT", abs(float(p["shoulder_cut_pct"])) / 100.0)
has_trail_pct = "trail_pct" in p and p.get("trail_pct") not in (None, "")
has_trail_arm = "trail_arm_pct" in p and p.get("trail_arm_pct") not in (None, "")
has_legacy = (
("trail_trigger" in p and p.get("trail_trigger") not in (None, ""))
or ("trail_stop" in p and p.get("trail_stop") not in (None, ""))
)
if has_trail_pct:
_set("US_MOMENTUM_TRAIL_PCT", abs(float(p["trail_pct"])) / 100.0)
elif has_legacy and not has_trail_arm:
patch["US_MOMENTUM_TRAIL_PCT"] = "0"
if has_trail_arm:
_set("US_MOMENTUM_TRAIL_ARM_PCT", abs(float(p["trail_arm_pct"])) / 100.0)
elif has_legacy and not has_trail_pct:
patch["US_MOMENTUM_TRAIL_ARM_PCT"] = "0"
if "ratchet_tiers" in p:
patch["US_MOMENTUM_RATCHET_TIERS"] = str(p.get("ratchet_tiers") or "").strip()
if "max_hold_bars" in p and p.get("max_hold_bars") not in (None, ""):
_set("US_MOMENTUM_MAX_HOLD_BARS", int(float(p["max_hold_bars"])))
for js_k, env_k in (
("use_defense_filters", "US_MOMENTUM_USE_DEFENSE_FILTERS"),
("use_high_chase_filter", "US_MOMENTUM_USE_HIGH_CHASE_FILTER"),
("use_daily_range_filter", "US_MOMENTUM_USE_DAILY_RANGE_FILTER"),
("use_ema_filter", "US_MOMENTUM_USE_EMA_FILTER"),
("use_rsi_max_filter", "US_MOMENTUM_USE_RSI_MAX_FILTER"),
("pattern_breakout", "US_MOMENTUM_PATTERN_BREAKOUT"),
("pattern_pullback", "US_MOMENTUM_PATTERN_PULLBACK"),
):
if js_k in p:
patch[env_k] = _env_bool_10(p.get(js_k))
if "chase_lookback_min" in p and p.get("chase_lookback_min") not in (None, ""):
_set("US_MOMENTUM_CHASE_LOOKBACK_MIN", int(float(p["chase_lookback_min"])))
if "pullback_lookback_min" in p and p.get("pullback_lookback_min") not in (None, ""):
_set("US_MOMENTUM_PULLBACK_LOOKBACK_MIN", int(float(p["pullback_lookback_min"])))
if "pullback_min_pct" in p and p.get("pullback_min_pct") not in (None, ""):
_set("US_MOMENTUM_PULLBACK_MIN_PCT", float(p["pullback_min_pct"]))
if "pullback_max_pct" in p and p.get("pullback_max_pct") not in (None, ""):
_set("US_MOMENTUM_PULLBACK_MAX_PCT", float(p["pullback_max_pct"]))
if "setup_vol_max_mult" in p and p.get("setup_vol_max_mult") not in (None, ""):
_set("US_MOMENTUM_SETUP_VOL_MAX_MULT", float(p["setup_vol_max_mult"]))
if "setup_bear_bars_min" in p and p.get("setup_bear_bars_min") not in (None, ""):
_set("US_MOMENTUM_SETUP_BEAR_BARS_MIN", int(float(p["setup_bear_bars_min"])))
if "ema_fast_period" in p and p.get("ema_fast_period") not in (None, ""):
_set("US_MOMENTUM_EMA_FAST_PERIOD", int(float(p["ema_fast_period"])))
if "ema_slow_period" in p and p.get("ema_slow_period") not in (None, ""):
_set("US_MOMENTUM_EMA_SLOW_PERIOD", int(float(p["ema_slow_period"])))
if "eod_enabled" in p:
patch["US_MOMENTUM_EOD_ENABLED"] = _env_bool_10(p.get("eod_enabled"))
if "eod_hm" in p and p.get("eod_hm") not in (None, ""):
eod_s = str(p["eod_hm"]).strip()
if eod_s and ":" not in eod_s and len(eod_s) == 4 and eod_s.isdigit():
eod_s = f"{eod_s[:2]}:{eod_s[2:]}"
patch["US_MOMENTUM_EOD_HM"] = eod_s
if "daily_profit_enabled" in p:
patch["US_MOMENTUM_DAILY_PROFIT_TARGET_ENABLED"] = (
"true" if _env_bool_10(p.get("daily_profit_enabled")) == "1" else "false"
)
if "daily_trail_tiers" in p:
tiers = str(p.get("daily_trail_tiers") or "").strip()
patch["US_MOMENTUM_DAILY_PROFIT_TRAIL_TIERS"] = "" if tiers.lower() == "off" else tiers
if tiers and tiers.lower() != "off":
patch["US_MOMENTUM_DAILY_PROFIT_MODE"] = (
str(p.get("daily_profit_mode") or "trailing").strip().lower() or "trailing"
)
elif "daily_profit_mode" in p:
patch["US_MOMENTUM_DAILY_PROFIT_MODE"] = (
str(p.get("daily_profit_mode") or "trailing").strip().lower() or "trailing"
)
# 수수료·SEC·환전 — UI% 이면 /100, 이미 비율(≤1)이면 그대로
def _ratio_from_ui_or_rate(key: str) -> Optional[float]:
if key not in p or p.get(key) in (None, ""):
return None
try:
x = abs(float(p[key]))
except (TypeError, ValueError):
return None
if x > 1.0:
return x / 100.0
return x
for ui_key, env_key in (
("fee_rate", "US_MOMENTUM_FEE_RATE"),
("sell_tax", "US_MOMENTUM_SELL_TAX"),
("fx_fee_rate", "US_MOMENTUM_FX_FEE_RATE"),
):
r = _ratio_from_ui_or_rate(ui_key)
if r is not None:
_set(env_key, r)
return patch
# 한투 미국주식 온라인 기준 기본값 (비율). DB 비어 있을 때 코드 폴백.
# 수수료 0.25% 편도 · SEC Fee 매도 0.00206% · 환전 스프레드 편도 0.05%(우대 95%≈1%×5%)
US_MOMENTUM_FEE_RATE_DEFAULT = 0.0025
US_MOMENTUM_SELL_TAX_DEFAULT = 0.0000206
US_MOMENTUM_FX_FEE_RATE_DEFAULT = 0.0005
def us_momentum_trading_cost_rates() -> Dict[str, float]:
"""실매·백테·Optuna 공통 — (수수료, SEC/매도세, 환전편도) 비율."""
from kis_trader.utils.env import get_env_float
fee = float(get_env_float("US_MOMENTUM_FEE_RATE", US_MOMENTUM_FEE_RATE_DEFAULT))
tax = float(get_env_float("US_MOMENTUM_SELL_TAX", US_MOMENTUM_SELL_TAX_DEFAULT))
fx = float(get_env_float("US_MOMENTUM_FX_FEE_RATE", US_MOMENTUM_FX_FEE_RATE_DEFAULT))
# 실수로 % 숫자(0.25)로 저장된 경우 → 비율로 정규화
if fee > 1.0:
fee = fee / 100.0
if tax > 1.0:
tax = tax / 100.0
if fx > 1.0:
fx = fx / 100.0
return {
"fee_rate": max(0.0, fee),
"sell_tax": max(0.0, tax),
"fx_fee_rate": max(0.0, fx),
}