Changes: - Introduced new files for strategy definitions and study names. - Enhanced `backtest_web.py` with functions to handle integer display prices and trade data formatting. - Updated backtesting logic to incorporate end-of-day (EOD) parameters for breakout and momentum strategies. - Added EOD configuration options in the database and parameter search files. Impact: - These changes improve the modularity and usability of the backtesting framework, allowing for better integration of EOD strategies and clearer trade data presentation.
445 lines
17 KiB
Python
445 lines
17 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_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_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 "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])
|
|
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", "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_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
|