472 lines
18 KiB
Python
472 lines
18 KiB
Python
"""
|
|
kis_trader/engine/tail_env_keys.py — 꼬리잡기(TAIL) env 키 단일 정의
|
|
====================================================================
|
|
레거시(MIN_DROP_RATE, STOP_LOSS_PCT 등) 제거 — TAIL_* 접두사만 사용.
|
|
config_short 테이블 저장 · tail_engine · 웹 · 파라서치 공통.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, Optional
|
|
|
|
# 레거시 → TAIL (1회 마이그레이션용, 코드 런타임 폴백 없음)
|
|
LEGACY_TO_TAIL: Dict[str, str] = {
|
|
"MIN_DROP_RATE": "TAIL_MIN_DROP_RATE",
|
|
"MIN_RECOVERY_RATIO_SHORT": "TAIL_MIN_RECOVERY_RATIO",
|
|
"MAX_RECOVERY_RATIO_3M": "TAIL_MAX_RECOVERY_3M",
|
|
"STOP_LOSS_PCT": "TAIL_STOP_LOSS_PCT",
|
|
"TAKE_PROFIT_PCT": "TAIL_TAKE_PROFIT_PCT",
|
|
"SHOULDER_MIN_HIGH_PCT": "TAIL_SHOULDER_MIN_HIGH_PCT",
|
|
"SHOULDER_CUT_PCT": "TAIL_SHOULDER_CUT_PCT",
|
|
"STOP_ATR_MULTIPLIER_TAIL": "TAIL_STOP_ATR_MULT",
|
|
"TARGET_ATR_MULTIPLIER_TAIL": "TAIL_TARGET_ATR_MULT",
|
|
"MIN_PRICE_TAIL": "TAIL_MIN_PRICE",
|
|
"MAX_DAILY_CHANGE_PCT": "TAIL_MAX_DAILY_CHG",
|
|
"MA20_MAX_ABOVE_PCT": "TAIL_MA20_MAX_ABOVE_PCT",
|
|
"MAX_LOSS_PER_TRADE_KRW": "TAIL_MAX_LOSS_KRW",
|
|
"MIN_DROP_PCT_FOR_LOSS_CUT": "TAIL_MIN_DROP_FOR_LOSS_CUT",
|
|
"RSI_OVERHEAT_THRESHOLD": "TAIL_RSI_THRESHOLD",
|
|
"RSI_PERIOD": "TAIL_RSI_PERIOD",
|
|
"REENTRY_COOLDOWN_SEC": "TAIL_COOLDOWN_SEC",
|
|
"MAX_DAILY_TAIL": "TAIL_MAX_DAILY",
|
|
"HIGH_PRICE_CHASE_THRESHOLD": "TAIL_HIGH_CHASE_THR",
|
|
"SHORT_TIME_START": "TAIL_TIME_START",
|
|
"SHORT_TIME_END": "TAIL_TIME_END",
|
|
# TIME_START/TIME_END 는 돌파·공통과 섞일 수 있어 마이그레이션 제외
|
|
"SHORT_MAX_STOCKS": "TAIL_MAX_STOCKS",
|
|
"SHORT_TOTAL_BUDGET_KRW": "TAIL_TOTAL_BUDGET_KRW",
|
|
"SHORT_MAX_BUY_AMOUNT": "TAIL_MAX_BUY_AMOUNT",
|
|
"TAIL_MAX_BUY_AMOUNT": "TAIL_MAX_BUY_AMOUNT",
|
|
"SHORT_ENTRY_MODE": "TAIL_ENTRY_MODE",
|
|
"SLOT_MONEY_DEFAULT": "TAIL_SLOT_MONEY",
|
|
"MAX_BUY_AMOUNT_PER_STOCK": "TAIL_SLOT_MONEY",
|
|
}
|
|
|
|
# config_short 전용 TAIL 키 (오케스트레이터 SHORT_* 제외)
|
|
TAIL_CONFIG_KEYS = frozenset({
|
|
"TAIL_MIN_DROP_RATE",
|
|
"TAIL_MIN_RECOVERY_RATIO",
|
|
"TAIL_MAX_RECOVERY_3M",
|
|
"TAIL_RATIO_MIN",
|
|
"TAIL_PCT_MIN",
|
|
"TAIL_STOP_LOSS_PCT",
|
|
"TAIL_TAKE_PROFIT_PCT",
|
|
"TAIL_SHOULDER_MIN_HIGH_PCT",
|
|
"TAIL_SHOULDER_CUT_PCT",
|
|
"TAIL_STOP_ATR_MULT",
|
|
"TAIL_TARGET_ATR_MULT",
|
|
"TAIL_ATR_SL_MIN_PCT",
|
|
"TAIL_ATR_SL_MAX_PCT",
|
|
"TAIL_ATR_TP_MIN_PCT",
|
|
"TAIL_ATR_TP_MAX_PCT",
|
|
"TAIL_MIN_PRICE",
|
|
"TAIL_MAX_DAILY_CHG",
|
|
"TAIL_MA20_MAX_ABOVE_PCT",
|
|
"TAIL_MAX_LOSS_KRW",
|
|
"TAIL_MIN_DROP_FOR_LOSS_CUT",
|
|
"TAIL_RSI_THRESHOLD",
|
|
"TAIL_RSI_PERIOD",
|
|
"TAIL_COOLDOWN_SEC",
|
|
"TAIL_MAX_DAILY",
|
|
"TAIL_SYMBOL_DAILY_LOSS_LIMIT_KRW",
|
|
"TAIL_SYMBOL_DAILY_LOSS_LIMIT_PCT",
|
|
"TAIL_REENTRY_MIN_EDGE_KRW",
|
|
"TAIL_REENTRY_REQUIRE_NONNEG",
|
|
"TAIL_HIGH_CHASE_THR",
|
|
"TAIL_TIME_START",
|
|
"TAIL_TIME_END",
|
|
"TAIL_MAX_STOCKS",
|
|
"TAIL_TOTAL_BUDGET_KRW",
|
|
"TAIL_MAX_BUY_AMOUNT",
|
|
"TAIL_SLOT_MONEY",
|
|
"TAIL_ENTRY_MODE",
|
|
"TAIL_LIMIT_ATR_MULT",
|
|
"TAIL_LIMIT_ANCHOR",
|
|
"TAIL_LIMIT_VALID_BARS",
|
|
"TAIL_LIMIT_FILL_SLIP_PCT",
|
|
"TAIL_SKIP_HTS_SCAN_DUPES",
|
|
"TAIL_USE_INTRADAY_DROP",
|
|
"TAIL_USE_MA20_FILTER",
|
|
"TAIL_USE_RSI_FILTER",
|
|
"TAIL_USE_DAILY_RANGE_FILTER",
|
|
"TAIL_USE_HIGH_CHASE_FILTER",
|
|
"TAIL_BAR_CHG_MIN_PCT",
|
|
"TAIL_BAR_CHG_MAX_PCT",
|
|
"TAIL_VOL_MULT",
|
|
"TAIL_VOL_WIN",
|
|
"TAIL_MIN_INVEST_RATIO_OF_SLOT",
|
|
"TAIL_CANDLE_LOOKBACK",
|
|
"TAIL_PATTERN_HAMMER",
|
|
"TAIL_PATTERN_PIN",
|
|
"TAIL_PATTERN_ENGULFING",
|
|
"TAIL_PATTERN_PIERCING",
|
|
"TAIL_PATTERN_HARAMI",
|
|
"TAIL_PATTERN_DOJI",
|
|
"TAIL_PATTERN_MORNING_STAR",
|
|
"TAIL_PIN_CLOSE_UPPER_RATIO",
|
|
"TAIL_PIN_MAX_UPPER_TAIL_RATIO",
|
|
"TAIL_ENGULF_MIN_BODY_RATIO",
|
|
"TAIL_PIERCING_PENETRATE_RATIO",
|
|
"TAIL_HARAMI_MAX_BODY_RATIO",
|
|
"TAIL_DOJI_BODY_MAX_RATIO",
|
|
"TAIL_MORNING_STAR_BODY_MAX_RATIO",
|
|
})
|
|
|
|
|
|
def _row_val(row: Dict[str, Any], key: str, default: Any = None) -> Any:
|
|
v = row.get(key)
|
|
if v not in (None, "", "None"):
|
|
return v
|
|
return default
|
|
|
|
|
|
def tail_env_float(row: Dict[str, Any], key: str, default: float) -> float:
|
|
v = _row_val(row, key)
|
|
if v is None:
|
|
return float(default)
|
|
try:
|
|
return float(v)
|
|
except (TypeError, ValueError):
|
|
return float(default)
|
|
|
|
|
|
def tail_env_int(row: Dict[str, Any], key: str, default: int) -> int:
|
|
v = _row_val(row, key)
|
|
if v is None:
|
|
return int(default)
|
|
try:
|
|
return int(float(v))
|
|
except (TypeError, ValueError):
|
|
return int(default)
|
|
|
|
|
|
def tail_market_time_hm() -> tuple:
|
|
"""
|
|
꼬리잡기(SHORT) 실매 장시간 HHMM — ``TAIL_TIME_START`` / ``TAIL_TIME_END`` 단일 소스.
|
|
|
|
글로벌 ``TIME_START`` / ``TIME_END``(스캘핑 레거시) 는 **폴백하지 않음**.
|
|
백테·tail_engine 기본값과 동일 (930~1500, DB에 값 있으면 DB 우선).
|
|
"""
|
|
from kis_trader.utils.env import get_env_int
|
|
|
|
start = get_env_int("TAIL_TIME_START", 930)
|
|
end = get_env_int("TAIL_TIME_END", 1500)
|
|
return int(start), int(end)
|
|
|
|
|
|
def tail_env_bool(row: Dict[str, Any], key: str, default: bool) -> bool:
|
|
v = _row_val(row, key)
|
|
if v is None:
|
|
return default
|
|
s = str(v).strip().lower()
|
|
if s in ("1", "true", "t", "y", "yes", "on"):
|
|
return True
|
|
if s in ("0", "false", "f", "n", "no", "off", ""):
|
|
return False
|
|
return default
|
|
|
|
|
|
def params_to_tail_env_patch(p: Dict[str, Any]) -> Dict[str, str]:
|
|
"""파라서치·웹 JSON params → TAIL env 패치 (문자열)."""
|
|
patch: Dict[str, str] = {}
|
|
if not p:
|
|
return patch
|
|
|
|
def _set(k: str, v: Any) -> None:
|
|
if v is not None and v != "":
|
|
patch[k] = str(v)
|
|
|
|
if "min_drop_rate" in p:
|
|
_set("TAIL_MIN_DROP_RATE", p["min_drop_rate"])
|
|
if "min_recovery_ratio" in p:
|
|
_set("TAIL_MIN_RECOVERY_RATIO", p["min_recovery_ratio"])
|
|
if "max_rec_3m" in p:
|
|
_set("TAIL_MAX_RECOVERY_3M", p["max_rec_3m"])
|
|
if "tail_ratio_min" in p:
|
|
_set("TAIL_RATIO_MIN", p["tail_ratio_min"])
|
|
if "tail_pct_min" in p:
|
|
_set("TAIL_PCT_MIN", p["tail_pct_min"])
|
|
if "sl_pct" in p:
|
|
_set("TAIL_STOP_LOSS_PCT", -abs(float(p["sl_pct"])))
|
|
if "tp_pct" in p:
|
|
_set("TAIL_TAKE_PROFIT_PCT", p["tp_pct"])
|
|
if "shoulder_cut_pct" in p:
|
|
_set("TAIL_SHOULDER_CUT_PCT", p["shoulder_cut_pct"])
|
|
if "shoulder_min_high" in p:
|
|
_set("TAIL_SHOULDER_MIN_HIGH_PCT", p["shoulder_min_high"])
|
|
if "trail_pct" in p:
|
|
_set("TAIL_TRAIL_PCT", abs(float(p["trail_pct"])))
|
|
if "trail_arm_pct" in p:
|
|
_set("TAIL_TRAIL_ARM_PCT", abs(float(p["trail_arm_pct"])))
|
|
if "ratchet_tiers" in p:
|
|
patch["TAIL_RATCHET_TIERS"] = str(p.get("ratchet_tiers") or "").strip()
|
|
if "max_hold_bars" in p and p["max_hold_bars"] is not None:
|
|
_set("TAIL_MAX_HOLD_BARS", int(float(p["max_hold_bars"])))
|
|
for js_k, env_k in (
|
|
("backtest_use_tick_db", "TAIL_BACKTEST_USE_TICK_DB"),
|
|
("backtest_use_tick_exit", "TAIL_BACKTEST_USE_TICK_EXIT"),
|
|
("backtest_tick_fallback_ohlc", "TAIL_BACKTEST_TICK_FALLBACK_OHLC"),
|
|
):
|
|
if js_k in p:
|
|
_set(env_k, str(p[js_k]).lower())
|
|
if "rsi_threshold" in p:
|
|
_set("TAIL_RSI_THRESHOLD", p["rsi_threshold"])
|
|
if "rsi_period" in p:
|
|
_set("TAIL_RSI_PERIOD", int(float(p["rsi_period"])))
|
|
if "ma20_max_above" in p:
|
|
_set("TAIL_MA20_MAX_ABOVE_PCT", p["ma20_max_above"])
|
|
if "max_daily_change" in p:
|
|
_set("TAIL_MAX_DAILY_CHG", p["max_daily_change"])
|
|
if "stop_atr_mult" in p:
|
|
_set("TAIL_STOP_ATR_MULT", p["stop_atr_mult"])
|
|
if "target_atr_mult" in p:
|
|
_set("TAIL_TARGET_ATR_MULT", p["target_atr_mult"])
|
|
for js_k, env_k in (
|
|
("atr_sl_min_pct", "TAIL_ATR_SL_MIN_PCT"),
|
|
("atr_sl_max_pct", "TAIL_ATR_SL_MAX_PCT"),
|
|
("atr_tp_min_pct", "TAIL_ATR_TP_MIN_PCT"),
|
|
("atr_tp_max_pct", "TAIL_ATR_TP_MAX_PCT"),
|
|
):
|
|
if js_k in p:
|
|
_set(env_k, p[js_k])
|
|
if "max_loss_krw" in p:
|
|
_set("TAIL_MAX_LOSS_KRW", int(float(p["max_loss_krw"])))
|
|
if "min_drop_pct_for_loss_cut" in p:
|
|
v = float(p["min_drop_pct_for_loss_cut"])
|
|
_set("TAIL_MIN_DROP_FOR_LOSS_CUT", round(v * 100, 2) if v < 1 else round(v, 2))
|
|
if "high_chase_thr" in p and p["high_chase_thr"] is not None:
|
|
_set("TAIL_HIGH_CHASE_THR", float(p["high_chase_thr"]))
|
|
if "cooldown_min" in p and p["cooldown_min"] is not None:
|
|
_set("TAIL_COOLDOWN_SEC", int(float(p["cooldown_min"]) * 60))
|
|
if "time_start_hm" in p and p["time_start_hm"] is not None:
|
|
_set("TAIL_TIME_START", int(float(p["time_start_hm"])))
|
|
if "time_end_hm" in p and p["time_end_hm"] is not None:
|
|
_set("TAIL_TIME_END", int(float(p["time_end_hm"])))
|
|
if "max_daily" in p and p["max_daily"] is not None:
|
|
_set("TAIL_MAX_DAILY", int(float(p["max_daily"])))
|
|
if "symbol_daily_loss_limit_krw" in p and p["symbol_daily_loss_limit_krw"] is not None:
|
|
_set("TAIL_SYMBOL_DAILY_LOSS_LIMIT_KRW", abs(float(p["symbol_daily_loss_limit_krw"])))
|
|
if "symbol_daily_loss_limit_pct" in p and p["symbol_daily_loss_limit_pct"] is not None:
|
|
_set("TAIL_SYMBOL_DAILY_LOSS_LIMIT_PCT", abs(float(p["symbol_daily_loss_limit_pct"])))
|
|
if "reentry_min_edge_krw" in p and p["reentry_min_edge_krw"] is not None:
|
|
_set("TAIL_REENTRY_MIN_EDGE_KRW", abs(float(p["reentry_min_edge_krw"])))
|
|
if "reentry_require_nonneg" in p:
|
|
_set("TAIL_REENTRY_REQUIRE_NONNEG", str(p["reentry_require_nonneg"]).lower())
|
|
if "eod_enabled" in p:
|
|
v = p.get("eod_enabled")
|
|
patch["TAIL_EOD_ENABLED"] = (
|
|
"1" if str(v).strip().lower() in ("1", "true", "t", "y", "yes", "on") else "0"
|
|
)
|
|
if "eod_hm" in p and p["eod_hm"] is not 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["TAIL_EOD_HM"] = eod_s
|
|
if "min_price" in p and p["min_price"] is not None:
|
|
_set("TAIL_MIN_PRICE", float(p["min_price"]))
|
|
if "slot_money" in p and p["slot_money"] is not None:
|
|
_set("TAIL_SLOT_MONEY", int(float(p["slot_money"])))
|
|
if "max_stocks" in p and p["max_stocks"] is not None:
|
|
_set("TAIL_MAX_STOCKS", int(float(p["max_stocks"])))
|
|
if "total_budget_krw" in p and p["total_budget_krw"] is not None:
|
|
_set("TAIL_TOTAL_BUDGET_KRW", int(float(p["total_budget_krw"])))
|
|
if "entry_mode" in p and p["entry_mode"] is not None:
|
|
_set("TAIL_ENTRY_MODE", str(p["entry_mode"]).strip().lower())
|
|
for js_k, env_k in (
|
|
("limit_atr_mult", "TAIL_LIMIT_ATR_MULT"),
|
|
("limit_anchor", "TAIL_LIMIT_ANCHOR"),
|
|
("limit_valid_bars", "TAIL_LIMIT_VALID_BARS"),
|
|
("limit_fill_slip_pct", "TAIL_LIMIT_FILL_SLIP_PCT"),
|
|
):
|
|
if js_k in p and p[js_k] is not None:
|
|
_set(env_k, p[js_k])
|
|
for js_k, env_k in (
|
|
("skip_hts_scan_dupes", "TAIL_SKIP_HTS_SCAN_DUPES"),
|
|
("use_intraday_drop", "TAIL_USE_INTRADAY_DROP"),
|
|
("use_ma20_filter", "TAIL_USE_MA20_FILTER"),
|
|
("use_rsi_filter", "TAIL_USE_RSI_FILTER"),
|
|
("use_daily_range_filter", "TAIL_USE_DAILY_RANGE_FILTER"),
|
|
("use_high_chase_filter", "TAIL_USE_HIGH_CHASE_FILTER"),
|
|
):
|
|
if js_k in p:
|
|
_set(env_k, str(p[js_k]).lower())
|
|
if "bar_chg_min_pct" in p:
|
|
_set("TAIL_BAR_CHG_MIN_PCT", p["bar_chg_min_pct"])
|
|
if "bar_chg_max_pct" in p:
|
|
_set("TAIL_BAR_CHG_MAX_PCT", p["bar_chg_max_pct"])
|
|
if "tail_vol_mult" in p:
|
|
_set("TAIL_VOL_MULT", p["tail_vol_mult"])
|
|
if "tail_vol_win" in p:
|
|
_set("TAIL_VOL_WIN", int(float(p["tail_vol_win"])))
|
|
for js_k, env_k in (
|
|
("pattern_hammer", "TAIL_PATTERN_HAMMER"),
|
|
("pattern_pin", "TAIL_PATTERN_PIN"),
|
|
("pattern_engulfing", "TAIL_PATTERN_ENGULFING"),
|
|
("pattern_piercing", "TAIL_PATTERN_PIERCING"),
|
|
("pattern_harami", "TAIL_PATTERN_HARAMI"),
|
|
("pattern_doji", "TAIL_PATTERN_DOJI"),
|
|
("pattern_morning_star", "TAIL_PATTERN_MORNING_STAR"),
|
|
):
|
|
if js_k in p:
|
|
_set(env_k, str(p[js_k]).lower())
|
|
for js_k, env_k in (
|
|
("pin_close_upper_ratio", "TAIL_PIN_CLOSE_UPPER_RATIO"),
|
|
("pin_max_upper_tail_ratio", "TAIL_PIN_MAX_UPPER_TAIL_RATIO"),
|
|
("engulf_min_body_ratio", "TAIL_ENGULF_MIN_BODY_RATIO"),
|
|
("piercing_penetrate_ratio", "TAIL_PIERCING_PENETRATE_RATIO"),
|
|
("harami_max_body_ratio", "TAIL_HARAMI_MAX_BODY_RATIO"),
|
|
("doji_body_max_ratio", "TAIL_DOJI_BODY_MAX_RATIO"),
|
|
("morning_star_body_max_ratio", "TAIL_MORNING_STAR_BODY_MAX_RATIO"),
|
|
("candle_lookback", "TAIL_CANDLE_LOOKBACK"),
|
|
):
|
|
if js_k in p and p[js_k] is not None:
|
|
_set(env_k, p[js_k])
|
|
# 휩쏘 TRIGGER (Optuna TPE / apply-best) — 전략 전용 TAIL_WHIPSAW_*
|
|
if "whipsaw_enabled" in p:
|
|
_set("TAIL_WHIPSAW_FILTER_ENABLED", str(p["whipsaw_enabled"]).lower())
|
|
for js_k, env_k in (
|
|
("whipsaw_subbar_sec", "TAIL_WHIPSAW_SUBBAR_SEC"),
|
|
("whipsaw_lookback_sec", "TAIL_WHIPSAW_LOOKBACK_SEC"),
|
|
("whipsaw_dip_pct", "TAIL_WHIPSAW_DIP_PCT"),
|
|
("whipsaw_recovery_tol_pct", "TAIL_WHIPSAW_RECOVERY_TOL_PCT"),
|
|
("whipsaw_min_subbars", "TAIL_WHIPSAW_MIN_SUBBARS"),
|
|
):
|
|
if js_k in p and p[js_k] is not None and str(p[js_k]).strip() != "":
|
|
_set(env_k, p[js_k])
|
|
return patch
|
|
|
|
|
|
def web_body_to_tail_env_patch(body: Dict[str, Any]) -> Dict[str, str]:
|
|
"""웹 saveTailConfig POST → TAIL env (퍼센트·분 단위 변환)."""
|
|
if not isinstance(body, dict):
|
|
return {}
|
|
|
|
def _get(key: str) -> Any:
|
|
v = body.get(key)
|
|
if v is None or v == "":
|
|
return None
|
|
return v
|
|
|
|
p: Dict[str, Any] = {}
|
|
md = _get("min_drop_rate")
|
|
if md is not None:
|
|
p["min_drop_rate"] = float(md) / 100.0
|
|
mr = _get("min_recovery_ratio")
|
|
if mr is not None:
|
|
p["min_recovery_ratio"] = float(mr) / 100.0
|
|
tr = _get("tail_ratio_min")
|
|
if tr is not None:
|
|
p["tail_ratio_min"] = float(tr)
|
|
tp = _get("tail_pct_min")
|
|
if tp is not None:
|
|
p["tail_pct_min"] = float(tp) / 100.0
|
|
m3 = _get("max_rec_3m")
|
|
if m3 is not None:
|
|
x = float(m3)
|
|
p["max_rec_3m"] = x if 0 < x <= 1 else x / 100.0
|
|
sl = _get("sl_pct")
|
|
if sl is not None:
|
|
p["sl_pct"] = float(sl) / 100.0
|
|
tpp = _get("tp_pct")
|
|
if tpp is not None:
|
|
p["tp_pct"] = float(tpp) / 100.0
|
|
smh = _get("shoulder_min_high")
|
|
if smh is not None:
|
|
p["shoulder_min_high"] = float(smh) / 100.0
|
|
sc = _get("shoulder_cut_pct")
|
|
if sc is not None:
|
|
p["shoulder_cut_pct"] = float(sc) / 100.0
|
|
trp = _get("trail_pct")
|
|
if trp is not None:
|
|
p["trail_pct"] = float(trp) / 100.0
|
|
tra = _get("trail_arm_pct")
|
|
if tra is not None:
|
|
p["trail_arm_pct"] = float(tra) / 100.0
|
|
hc = _get("high_chase_thr")
|
|
if hc is not None:
|
|
x = float(hc)
|
|
p["high_chase_thr"] = x if 0 < x <= 1 else x / 100.0
|
|
for k in (
|
|
"cooldown_min", "rsi_threshold", "rsi_period", "time_start", "time_end",
|
|
"max_daily", "symbol_daily_loss_limit_krw", "symbol_daily_loss_limit_pct",
|
|
"reentry_min_edge_krw", "reentry_require_nonneg",
|
|
"min_price", "max_daily_change", "ma20_max_above",
|
|
"max_loss_krw", "stop_atr_mult", "target_atr_mult",
|
|
"atr_sl_min_pct", "atr_sl_max_pct", "atr_tp_min_pct", "atr_tp_max_pct",
|
|
"slot_money", "max_stocks", "total_budget_krw", "entry_mode",
|
|
"limit_atr_mult", "limit_anchor", "limit_valid_bars", "limit_fill_slip_pct",
|
|
"skip_hts_scan_dupes", "use_intraday_drop", "use_ma20_filter",
|
|
"use_rsi_filter", "use_daily_range_filter", "use_high_chase_filter",
|
|
"bar_chg_min_pct", "bar_chg_max_pct", "max_hold_bars",
|
|
"tail_vol_mult", "tail_vol_win",
|
|
"backtest_use_tick_db", "backtest_use_tick_exit", "backtest_tick_fallback_ohlc",
|
|
"pattern_hammer", "pattern_pin", "pattern_engulfing", "pattern_piercing",
|
|
"pattern_harami", "pattern_doji", "pattern_morning_star",
|
|
"pin_close_upper_ratio", "pin_max_upper_tail_ratio",
|
|
"engulf_min_body_ratio", "piercing_penetrate_ratio",
|
|
"harami_max_body_ratio", "doji_body_max_ratio",
|
|
"morning_star_body_max_ratio", "candle_lookback",
|
|
):
|
|
v = _get(k)
|
|
if v is not None:
|
|
p[k] = v
|
|
if "ratchet_tiers" in body:
|
|
p["ratchet_tiers"] = str(body.get("ratchet_tiers") or "").strip()
|
|
if "eod_enabled" in body:
|
|
p["eod_enabled"] = body.get("eod_enabled")
|
|
eod_hm = _get("eod_hm")
|
|
if eod_hm is not None:
|
|
p["eod_hm"] = str(eod_hm).strip()
|
|
mdloss = _get("min_drop_pct_for_loss_cut")
|
|
if mdloss is not None:
|
|
p["min_drop_pct_for_loss_cut"] = float(mdloss)
|
|
if "time_start" in p:
|
|
p["time_start_hm"] = int(float(p.pop("time_start")))
|
|
if "time_end" in p:
|
|
p["time_end_hm"] = int(float(p.pop("time_end")))
|
|
return params_to_tail_env_patch(p)
|
|
|
|
|
|
def migrate_config_short_legacy(db=None) -> Dict[str, str]:
|
|
"""config_short 최신 행: 레거시 값 → TAIL_* 복사 후 UPDATE."""
|
|
own_db = None
|
|
applied: Dict[str, str] = {}
|
|
try:
|
|
if db is None:
|
|
from database import TradeDB
|
|
own_db = TradeDB()
|
|
db = own_db
|
|
row = db.conn.execute(
|
|
"SELECT * FROM config_short ORDER BY id DESC LIMIT 1"
|
|
).fetchone()
|
|
if not row:
|
|
return applied
|
|
r = dict(row)
|
|
rid = r.get("id")
|
|
updates: Dict[str, str] = {}
|
|
for old_k, new_k in LEGACY_TO_TAIL.items():
|
|
if new_k in updates:
|
|
continue
|
|
ov = r.get(old_k)
|
|
nv = r.get(new_k)
|
|
if ov not in (None, "", "None") and (nv in (None, "", "None")):
|
|
updates[new_k] = str(ov)
|
|
if not updates or rid is None:
|
|
return applied
|
|
cols = ", ".join(f"{k}=%s" for k in updates)
|
|
db.conn.execute(
|
|
f"UPDATE config_short SET {cols} WHERE id=%s",
|
|
list(updates.values()) + [rid],
|
|
)
|
|
try:
|
|
db.conn.commit()
|
|
except Exception:
|
|
pass
|
|
applied = updates
|
|
finally:
|
|
if own_db is not None:
|
|
try:
|
|
own_db.close()
|
|
except Exception:
|
|
pass
|
|
return applied
|