feat(tests): 신규 키움 웹소켓 조건검색 및 실시간 조건검색 테스트 추가
변경 사항 ---- - _test_kiwoom_condition_list.py: 키움 웹소켓 조건검색 '목록조회' 기능을 단독으로 테스트하는 스크립트 추가 - _test_kiwoom_condition_realtime.py: 'momentum' 조건식을 실시간으로 등록하고 초기 매칭 종목 리스트 및 실시간 편입/이탈을 수신하는 테스트 스크립트 추가 - _verify_columnar_bitid.py, _verify_shared_e2e_breakout.py, _verify_shared_e2e.py: 공유 메모리 및 dict 간의 데이터 일관성을 검증하는 테스트 추가 영향 ---- - 신규 테스트 스크립트 추가로 키움 웹소켓 API의 기능 검증 및 안정성을 높임 - 기존 기능에 대한 영향 없음 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
618
kis_trader/backtest/param_search_apply_snapshot.py
Normal file
618
kis_trader/backtest/param_search_apply_snapshot.py
Normal file
@@ -0,0 +1,618 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
param_search_apply_snapshot.py — 파라미터 탐색 결과 JSON → insert_env_snapshot (config_* + env_config)
|
||||
===========================================================================================
|
||||
지원 전략 (파일·내용 자동 판별):
|
||||
|
||||
MOMENTUM search_momentum_*.json top[].merged_params
|
||||
SCALP search_*.json (rsi_oversold) top[].db_snapshot 우선, 없으면 merged에서 생성
|
||||
BREAKOUT search_breakout_*.json top[].merged_params
|
||||
TAIL search_tail_*.json results[N-1].params (정렬된 순서)
|
||||
UPDOW updow_param_*.json top[N-1].apply_cfg + tf
|
||||
|
||||
사용 예:
|
||||
cd /path/to/kis_bot
|
||||
python3 kis_trader/backtest/param_search_apply_snapshot.py \\
|
||||
--json kis_trader/backtest/results/search_momentum_coarse_20260514_150927.json --rank 1
|
||||
|
||||
python3 kis_trader/backtest/param_search_apply_snapshot.py \\
|
||||
--json kis_trader/backtest/results/search_coarse_20260509_183233.json --rank 1 --dry-run
|
||||
|
||||
python3 kis_trader/backtest/param_search_apply_snapshot.py \\
|
||||
--json results/search_tail_coarse_*.json --rank 3 --allow-non-positive-pnl
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(HERE))
|
||||
for _p in (ROOT, HERE):
|
||||
if _p not in sys.path:
|
||||
sys.path.insert(0, _p)
|
||||
|
||||
from database import TradeDB # noqa: E402
|
||||
from kis_trader.backtest.backtest_portfolio_common import ( # noqa: E402
|
||||
merge_param_search_apply_source,
|
||||
portfolio_env_patch,
|
||||
session_env_patch,
|
||||
)
|
||||
|
||||
|
||||
def _env_bool_10(v: Any) -> str:
|
||||
if isinstance(v, bool):
|
||||
return "1" if v else "0"
|
||||
s = str(v).strip().lower()
|
||||
return "1" if s in ("1", "true", "t", "y", "yes", "on") else "0"
|
||||
|
||||
|
||||
def _detect_strategy(data: Dict[str, Any], path: str) -> str:
|
||||
base = os.path.basename(path).lower()
|
||||
s = (data.get("strategy") or "").strip().upper()
|
||||
if s in ("MOMENTUM", "BREAKOUT", "SCALP", "TAIL", "UPDOW"):
|
||||
return s
|
||||
|
||||
if base.startswith("updow_param_"):
|
||||
return "UPDOW"
|
||||
if base.startswith("search_tail_") or base.startswith("tail_search_"):
|
||||
return "TAIL"
|
||||
if base.startswith("search_breakout_"):
|
||||
return "BREAKOUT"
|
||||
if base.startswith("search_momentum_"):
|
||||
return "MOMENTUM"
|
||||
|
||||
if isinstance(data.get("code"), str) and len(str(data.get("code")).strip()) == 6:
|
||||
if "tf" in data and isinstance(data.get("top"), list):
|
||||
return "UPDOW"
|
||||
|
||||
results = data.get("results")
|
||||
if isinstance(results, list) and results:
|
||||
p0 = results[0].get("params") or {}
|
||||
if isinstance(p0, dict) and (
|
||||
"min_drop_rate" in p0 or "tail_ratio_min" in p0 or "shoulder_cut_pct" in p0
|
||||
):
|
||||
return "TAIL"
|
||||
|
||||
top = data.get("top")
|
||||
if isinstance(top, list) and top:
|
||||
merged = top[0].get("merged_params") or {}
|
||||
params = top[0].get("params") or {}
|
||||
ref = merged if isinstance(merged, dict) else {}
|
||||
if not ref:
|
||||
ref = params if isinstance(params, dict) else {}
|
||||
if "mom_rsi_min" in ref:
|
||||
return "MOMENTUM"
|
||||
if "prev_chg_min" in ref:
|
||||
return "BREAKOUT"
|
||||
if "rsi_oversold" in ref:
|
||||
return "SCALP"
|
||||
|
||||
return "UNKNOWN"
|
||||
|
||||
|
||||
def _patch_from_momentum_merged(m: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""merged_params(UI 퍼센트 등) → env_config 문자열 패치 (웹 momentum 저장과 동일 계열)."""
|
||||
patch: Dict[str, str] = {}
|
||||
|
||||
def gi(key: str, default: int = 0) -> int:
|
||||
v = m.get(key)
|
||||
return int(float(v)) if v not in (None, "") else default
|
||||
|
||||
def gf(key: str, default: float = 0.0) -> float:
|
||||
v = m.get(key)
|
||||
return float(v) if v not in (None, "") else default
|
||||
|
||||
patch["MOMENTUM_RSI_MIN"] = str(gi("mom_rsi_min", 50))
|
||||
patch["MOMENTUM_RSI_MAX"] = str(gi("mom_rsi_max", 80))
|
||||
patch["SCALP_MOM_RSI_MIN"] = patch["MOMENTUM_RSI_MIN"]
|
||||
patch["SCALP_MOM_RSI_MAX"] = patch["MOMENTUM_RSI_MAX"]
|
||||
|
||||
patch["MOMENTUM_VOL_MULT"] = str(gf("mom_vol_mult", 1.5))
|
||||
patch["SCALP_MOM_VOL_MULT"] = patch["MOMENTUM_VOL_MULT"]
|
||||
|
||||
patch["MOMENTUM_VOL_WIN"] = str(gi("mom_vol_win", 5))
|
||||
patch["SCALP_MOM_VOL_WIN"] = patch["MOMENTUM_VOL_WIN"]
|
||||
|
||||
patch["MOMENTUM_TIME_END_HM"] = str(gi("mom_time_end_hm", 1430))
|
||||
patch["SCALP_MOM_TIME_END_HM"] = patch["MOMENTUM_TIME_END_HM"]
|
||||
|
||||
ts = m.get("time_start_hm")
|
||||
if ts not in (None, ""):
|
||||
patch["MOMENTUM_TIME_START"] = str(int(float(ts)))
|
||||
|
||||
sl_r = str(abs(gf("sl_pct", 1.5)) / 100.0)
|
||||
tp_r = str(abs(gf("tp_pct", 2.5)) / 100.0)
|
||||
patch["MOMENTUM_STOP_LOSS_PCT"] = sl_r
|
||||
patch["SCALP_STOP_LOSS_PCT"] = sl_r
|
||||
patch["MOMENTUM_TAKE_PROFIT_PCT"] = tp_r
|
||||
patch["SCALP_TAKE_PROFIT_PCT"] = tp_r
|
||||
|
||||
patch["SCALP_ATR_UP_MULT"] = str(abs(gf("trail_trigger", 0.7)) / 100.0)
|
||||
patch["SCALP_ATR_DOWN_MULT"] = str(abs(gf("trail_stop", 0.4)) / 100.0)
|
||||
|
||||
patch["SCALP_COOLDOWN_SEC"] = str(int(float(gf("cooldown_min", 10)) * 60))
|
||||
|
||||
md = str(gi("max_daily", 5))
|
||||
patch["MOMENTUM_MAX_DAILY"] = md
|
||||
patch["SCALP_MAX_DAILY"] = md
|
||||
|
||||
sm = m.get("slot_money")
|
||||
if sm not in (None, ""):
|
||||
sms = str(int(float(sm)))
|
||||
patch["MOMENTUM_SLOT_MONEY"] = sms
|
||||
patch["SLOT_MONEY_DEFAULT"] = sms
|
||||
patch["MOMENTUM_MAX_BUY_AMOUNT"] = sms
|
||||
patch["MAX_BUY_AMOUNT_PER_STOCK"] = sms
|
||||
|
||||
hc = gf("high_chase_thr", 0.96)
|
||||
ratio_hc = hc if 0 < hc <= 1 else hc / 100.0
|
||||
sr = str(ratio_hc)
|
||||
patch["HIGH_CHASE_THR"] = sr
|
||||
patch["SCALP_HIGH_PRICE_CHASE_THRESHOLD"] = sr
|
||||
patch["HIGH_PRICE_CHASE_THRESHOLD"] = sr
|
||||
|
||||
vchg = str(gf("max_daily_chg", 20.0))
|
||||
patch["MAX_DAILY_CHG"] = vchg
|
||||
patch["SCALP_MAX_DAILY_CHANGE_PCT"] = vchg
|
||||
patch["MAX_DAILY_CHANGE_PCT"] = vchg
|
||||
|
||||
mp = str(gf("min_price", 1000))
|
||||
patch["MOMENTUM_MIN_PRICE"] = mp
|
||||
patch["SCALP_MIN_PRICE"] = mp
|
||||
|
||||
ml = str(int(float(m.get("max_loss_krw") or 200000)))
|
||||
patch["MOMENTUM_MAX_LOSS_PER_TRADE_KRW"] = ml
|
||||
patch["SCALP_MAX_LOSS_PER_TRADE_KRW"] = ml
|
||||
patch["MAX_LOSS_PER_TRADE_KRW"] = ml
|
||||
|
||||
mm = str(gf("min_margin", 0.2))
|
||||
patch["MOMENTUM_MIN_PROFIT_PCT"] = mm
|
||||
patch["SCALP_MIN_PROFIT_PCT"] = mm
|
||||
|
||||
if "use_defense_filters" in m:
|
||||
patch["SCALP_USE_DEFENSE_FILTERS"] = _env_bool_10(m.get("use_defense_filters"))
|
||||
|
||||
if "mom_max_from_open_pct" in m and m.get("mom_max_from_open_pct") not in (None, ""):
|
||||
patch["MOMENTUM_MAX_FROM_OPEN_PCT"] = str(float(m["mom_max_from_open_pct"]))
|
||||
if "mom_min_from_open_pct" in m and m.get("mom_min_from_open_pct") not in (None, ""):
|
||||
patch["MOMENTUM_MIN_FROM_OPEN_PCT"] = str(float(m["mom_min_from_open_pct"]))
|
||||
|
||||
x = m.get("tp_max_pct")
|
||||
if x not in (None, ""):
|
||||
sr = str(abs(float(x)) / 100.0)
|
||||
patch["MOMENTUM_TP_MAX_PCT"] = sr
|
||||
patch["SCALP_TP_MAX_PCT"] = sr
|
||||
|
||||
x = m.get("shoulder_min_high")
|
||||
if x not in (None, ""):
|
||||
sr = str(abs(float(x)) / 100.0)
|
||||
patch["MOMENTUM_SHOULDER_MIN_HIGH_PCT"] = sr
|
||||
patch["SCALP_SHOULDER_MIN_HIGH_PCT"] = sr
|
||||
patch["SHOULDER_MIN_HIGH_PCT"] = sr
|
||||
|
||||
x = m.get("shoulder_cut_pct")
|
||||
if x not in (None, ""):
|
||||
sr = str(abs(float(x)) / 100.0)
|
||||
patch["MOMENTUM_SHOULDER_CUT_PCT"] = sr
|
||||
patch["SCALP_SHOULDER_CUT_PCT"] = sr
|
||||
patch["SHOULDER_CUT_PCT"] = sr
|
||||
|
||||
# 전용 트레일(momentum_engine) — UI 퍼센트(0.5) → 엔진 비율(0.005) 저장.
|
||||
# (기존엔 trail_trigger/stop=SCALP_ATR_*만 기록돼 MOMENTUM_TRAIL_PCT가 0으로 남던 버그 수정)
|
||||
x = m.get("trail_pct")
|
||||
if x not in (None, ""):
|
||||
patch["MOMENTUM_TRAIL_PCT"] = str(abs(float(x)) / 100.0)
|
||||
x = m.get("trail_arm_pct")
|
||||
if x not in (None, ""):
|
||||
patch["MOMENTUM_TRAIL_ARM_PCT"] = str(abs(float(x)) / 100.0)
|
||||
|
||||
if "use_ema_filter" in m:
|
||||
patch["MOMENTUM_USE_EMA_FILTER"] = _env_bool_10(m.get("use_ema_filter"))
|
||||
if "use_rsi_max_filter" in m:
|
||||
patch["MOMENTUM_USE_RSI_MAX_FILTER"] = _env_bool_10(m.get("use_rsi_max_filter"))
|
||||
if "pattern_breakout" in m:
|
||||
patch["MOMENTUM_PATTERN_BREAKOUT"] = _env_bool_10(m.get("pattern_breakout"))
|
||||
if "pattern_pullback" in m:
|
||||
patch["MOMENTUM_PATTERN_PULLBACK"] = _env_bool_10(m.get("pattern_pullback"))
|
||||
if "use_high_chase_filter" in m:
|
||||
patch["MOMENTUM_USE_HIGH_CHASE_FILTER"] = _env_bool_10(m.get("use_high_chase_filter"))
|
||||
if "use_daily_range_filter" in m:
|
||||
patch["MOMENTUM_USE_DAILY_RANGE_FILTER"] = _env_bool_10(m.get("use_daily_range_filter"))
|
||||
x = m.get("chase_lookback_min")
|
||||
if x not in (None, ""):
|
||||
patch["MOMENTUM_CHASE_LOOKBACK_MIN"] = str(int(float(x)))
|
||||
x = m.get("pullback_lookback_min")
|
||||
if x not in (None, ""):
|
||||
patch["MOMENTUM_PULLBACK_LOOKBACK_MIN"] = str(int(float(x)))
|
||||
x = m.get("pullback_min_pct")
|
||||
if x not in (None, ""):
|
||||
patch["MOMENTUM_PULLBACK_MIN_PCT"] = str(float(x))
|
||||
x = m.get("pullback_max_pct")
|
||||
if x not in (None, ""):
|
||||
patch["MOMENTUM_PULLBACK_MAX_PCT"] = str(float(x))
|
||||
x = m.get("ema_fast_period")
|
||||
if x not in (None, ""):
|
||||
patch["MOMENTUM_EMA_FAST_PERIOD"] = str(int(float(x)))
|
||||
x = m.get("ema_slow_period")
|
||||
if x not in (None, ""):
|
||||
patch["MOMENTUM_EMA_SLOW_PERIOD"] = str(int(float(x)))
|
||||
|
||||
return patch
|
||||
|
||||
|
||||
def apply_env_patch(patch: Dict[str, str]) -> Optional[int]:
|
||||
"""병합 스냅샷에 patch 반영 후 insert_env_snapshot — config_scalp/momentum/… 분리 저장."""
|
||||
if not patch:
|
||||
return None
|
||||
db = TradeDB()
|
||||
try:
|
||||
latest = db.get_latest_env()
|
||||
snap = dict(latest["snapshot"]) if latest else {}
|
||||
snap.update(patch)
|
||||
return db.insert_env_snapshot(snap)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def _patch_from_breakout_merged(m: Dict[str, Any]) -> Dict[str, str]:
|
||||
patch: Dict[str, str] = {}
|
||||
|
||||
def gv(key: str) -> Optional[Any]:
|
||||
v = m.get(key)
|
||||
return None if v in (None, "") else v
|
||||
|
||||
x = gv("lookback_min")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_LOOKBACK_MIN"] = str(int(float(x)))
|
||||
x = gv("vol_window")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_VOL_WIN"] = str(int(float(x)))
|
||||
x = gv("vol_mult")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_VOL_MULT"] = str(float(x))
|
||||
x = gv("min_turnover_1m_pct")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_MIN_TURNOVER_1M_PCT"] = str(float(x))
|
||||
x = gv("prev_chg_min")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_PREV_CHG_MIN"] = str(float(x))
|
||||
x = gv("prev_chg_max")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_PREV_CHG_MAX"] = str(float(x))
|
||||
|
||||
x = gv("sl_pct")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_STOP_LOSS_PCT"] = str(-abs(float(x)) / 100.0)
|
||||
x = gv("tp_pct")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_TAKE_PROFIT_PCT"] = str(abs(float(x)) / 100.0)
|
||||
x = gv("trail_pct")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_TRAIL_PCT"] = str(abs(float(x)) / 100.0)
|
||||
x = gv("trail_arm_pct")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_TRAIL_ARM_PCT"] = str(abs(float(x)) / 100.0)
|
||||
x = gv("shoulder_min_high_pct")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_SHOULDER_MIN_HIGH_PCT"] = str(abs(float(x)) / 100.0)
|
||||
x = gv("shoulder_cut_pct")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_SHOULDER_CUT_PCT"] = str(abs(float(x)) / 100.0)
|
||||
# ── ATR 동적 손절 (sl_min/max 는 % 단위 그대로 — 엔진 _breakout_sl_line 가 /100) ──
|
||||
x = gv("sl_mode")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_SL_MODE"] = str(x).strip().lower()
|
||||
x = gv("atr_period")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_ATR_PERIOD"] = str(int(float(x)))
|
||||
x = gv("atr_sl_mult")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_ATR_SL_MULT"] = str(float(x))
|
||||
x = gv("atr_sl_min_pct")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_ATR_SL_MIN_PCT"] = str(float(x))
|
||||
x = gv("atr_sl_max_pct")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_ATR_SL_MAX_PCT"] = str(float(x))
|
||||
# 래칫(단계식 트레일) 문자열 — % 단위 그대로 저장(엔진 _breakout_ratchet_tiers 가 /100).
|
||||
# 빈 문자열("")=OFF 도 명시 저장하여, 기존 DB 래칫을 1위가 OFF 일 때 확실히 끈다.
|
||||
if "ratchet_tiers" in m:
|
||||
patch["BREAKOUT_RATCHET_TIERS"] = str(m.get("ratchet_tiers") or "").strip()
|
||||
x = gv("max_hold_bars")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_MAX_HOLD_BARS"] = str(int(float(x)))
|
||||
|
||||
# 가짜돌파(휩쏘) 필터 — % 그대로 저장
|
||||
x = gv("confirm_margin_pct")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_CONFIRM_MARGIN_PCT"] = str(float(x))
|
||||
x = gv("body_min_pct")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_BODY_MIN_PCT"] = str(float(x))
|
||||
|
||||
x = gv("time_start_hm")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_TIME_START"] = str(int(float(x)))
|
||||
x = gv("time_end_hm")
|
||||
if x is not None:
|
||||
te = str(int(float(x)))
|
||||
patch["BREAKOUT_TIME_END"] = te
|
||||
patch["BREAKOUT_GOLDEN_END_HM"] = te
|
||||
|
||||
x = gv("max_daily")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_MAX_DAILY"] = str(int(float(x)))
|
||||
x = gv("cooldown_min")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_COOLDOWN_SEC"] = str(int(float(x) * 60))
|
||||
x = gv("max_daily_chg")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_MAX_DAILY_CHG"] = str(float(x))
|
||||
x = gv("min_price")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_MIN_PRICE"] = str(int(float(x)))
|
||||
x = gv("entry_mode")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_ENTRY_MODE"] = str(x).strip().lower()
|
||||
x = gv("intrabar_slippage_pct")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_INTRABAR_SLIPPAGE_PCT"] = str(float(x))
|
||||
x = gv("max_loss_krw")
|
||||
sm_raw = gv("slot_money")
|
||||
ml_i = None
|
||||
if x is not None:
|
||||
from kis_trader.strategies.breakout import ( # noqa: WPS433
|
||||
breakout_invest_amount_krw,
|
||||
normalize_breakout_max_loss_krw,
|
||||
)
|
||||
ml_i = normalize_breakout_max_loss_krw(x)
|
||||
patch["BREAKOUT_MAX_LOSS_PER_TRADE_KRW"] = str(ml_i)
|
||||
patch["MAX_LOSS_PER_TRADE_KRW"] = str(ml_i)
|
||||
if sm_raw is not None:
|
||||
cap = int(float(sm_raw))
|
||||
patch["BREAKOUT_SLOT_MONEY"] = str(cap)
|
||||
patch["SLOT_MONEY_DEFAULT"] = str(cap)
|
||||
patch["BREAKOUT_MAX_BUY_AMOUNT"] = str(cap)
|
||||
patch["MAX_BUY_AMOUNT_PER_STOCK"] = str(cap)
|
||||
elif ml_i is not None:
|
||||
from kis_trader.strategies.breakout import breakout_invest_amount_krw # noqa: WPS433
|
||||
sl_ui = float(m.get("sl_pct") or 1.5)
|
||||
cap = int(breakout_invest_amount_krw(ml_i, sl_ui, 2_000_000))
|
||||
patch["BREAKOUT_SLOT_MONEY"] = str(cap)
|
||||
patch["SLOT_MONEY_DEFAULT"] = str(cap)
|
||||
patch["BREAKOUT_MAX_BUY_AMOUNT"] = str(cap)
|
||||
|
||||
if "use_ema_filter" in m:
|
||||
patch["BREAKOUT_USE_EMA_FILTER"] = _env_bool_10(m.get("use_ema_filter"))
|
||||
x = gv("ema_fast_period")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_EMA_FAST_PERIOD"] = str(int(float(x)))
|
||||
x = gv("ema_slow_period")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_EMA_SLOW_PERIOD"] = str(int(float(x)))
|
||||
|
||||
return patch
|
||||
|
||||
|
||||
def _apply_tail_params(params: Dict[str, Any]) -> None:
|
||||
"""tail_param_search.apply_params_to_db 와 동일 경로 (단일 INSERT)."""
|
||||
import tail_param_search as tps # noqa: WPS433 — 동일 디렉터리 스크립트
|
||||
|
||||
tps.apply_params_to_db(params)
|
||||
|
||||
|
||||
def _apply_updow_row(row: Dict[str, Any], tf: int) -> Dict[str, str]:
|
||||
from kis_trader.strategies import updow_buy as udb
|
||||
|
||||
ac = row.get("apply_cfg") or {}
|
||||
if not isinstance(ac, dict):
|
||||
return {}
|
||||
return udb.env_snapshot_patch_from_engine_cfg(ac, tf_min=tf)
|
||||
|
||||
|
||||
def _apply_updow_stock(code: str, row: Dict[str, Any], tf: int, dry_run: bool) -> None:
|
||||
"""탐색 ``apply_cfg`` → ``updow_stock_config`` (하락매수 종목 단일 소스)."""
|
||||
from kis_trader.strategies import updow_holding_cfg as uhc
|
||||
|
||||
ac = row.get("apply_cfg") or {}
|
||||
if not isinstance(ac, dict) or not ac:
|
||||
print("⚠️ updow_stock_config 미반영: apply_cfg 없음")
|
||||
return
|
||||
code = str(code).strip()
|
||||
if len(code) != 6:
|
||||
print("⚠️ updow_stock_config 미반영: code 없음/형식 오류")
|
||||
return
|
||||
if dry_run:
|
||||
print(f"[dry-run] updow_stock_config {code} ← apply_cfg keys={list(ac.keys())}")
|
||||
return
|
||||
db = TradeDB()
|
||||
try:
|
||||
import holding_bot as hb
|
||||
|
||||
uhc.ensure_updow_stock_config_table(db)
|
||||
if tf not in hb.KIWOOM_MINUTE_TICS:
|
||||
tf = 60
|
||||
meta = uhc.get_updow_stock_meta(db, code)
|
||||
name = str(meta.get("name") if meta else code).strip() or code
|
||||
uhc.set_updow_stock_config(db, code, name, ac, tf_min=tf)
|
||||
mh = int(float(ac.get("max_hold_bars", 16)))
|
||||
print(f"✅ updow_stock_config 저장 ({code}, tf={tf}, max_hold={mh})")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def main(argv: Optional[List[str]] = None) -> int:
|
||||
ap = argparse.ArgumentParser(
|
||||
description="파라미터 탐색 JSON의 N위 조합을 env_config 에 INSERT (최신 행 복사 후 패치)",
|
||||
)
|
||||
ap.add_argument("--json", required=True, help="결과 JSON 경로")
|
||||
ap.add_argument("--rank", type=int, default=1, help="순위 (1부터). TAIL은 results 정렬 기준")
|
||||
ap.add_argument("--dry-run", action="store_true", help="INSERT 생략, 패치 키만 출력")
|
||||
ap.add_argument(
|
||||
"--allow-non-positive-pnl",
|
||||
action="store_true",
|
||||
help="total_pnl<=0 이더라도 적용 (기본: 해당 시 경고 후 종료)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--env",
|
||||
action="store_true",
|
||||
help="UPDOW: env_config 전역 UPDOW_* 도 함께 갱신 (기본: updow_stock_config 만)",
|
||||
)
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
path = os.path.abspath(args.json)
|
||||
if not os.path.isfile(path):
|
||||
print(f"❌ 파일 없음: {path}")
|
||||
return 2
|
||||
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"❌ JSON 로드 실패: {e}")
|
||||
return 3
|
||||
|
||||
strategy = _detect_strategy(data, path)
|
||||
if strategy == "UNKNOWN":
|
||||
print("❌ 전략을 자동 판별하지 못했습니다. 파일명·키 구조를 확인하세요.")
|
||||
return 4
|
||||
|
||||
rank = max(1, int(args.rank))
|
||||
print(f"📂 {path}")
|
||||
print(f"📌 전략={strategy} rank={rank}")
|
||||
|
||||
# ── TAIL: 별도 모듈이 INSERT 수행 ─────────────────────────────
|
||||
if strategy == "TAIL":
|
||||
results = data.get("results") or []
|
||||
if rank > len(results):
|
||||
print(f"❌ rank 범위 초과 (1~{len(results)})")
|
||||
return 5
|
||||
target = results[rank - 1]
|
||||
pnl = int(target.get("total_pnl") or 0)
|
||||
if pnl <= 0 and not args.allow_non_positive_pnl:
|
||||
print(f"⚠️ total_pnl={pnl} ≤ 0 → 중단. 적용하려면 --allow-non-positive-pnl")
|
||||
return 6
|
||||
params = target.get("params") or {}
|
||||
if args.dry_run:
|
||||
merged = merge_param_search_apply_source(target, data)
|
||||
print("[dry-run] TAIL merged:", json.dumps(merged, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
merged = merge_param_search_apply_source(target, data)
|
||||
_apply_tail_params(merged)
|
||||
print("✅ TAIL env_config INSERT 완료 (tail_param_search.apply_params_to_db)")
|
||||
return 0
|
||||
|
||||
# ── UPDOW ─────────────────────────────────────────────────────
|
||||
if strategy == "UPDOW":
|
||||
top = data.get("top") or []
|
||||
if rank > len(top):
|
||||
print(f"❌ rank 범위 초과 (1~{len(top)})")
|
||||
return 5
|
||||
row = top[rank - 1]
|
||||
pnl = int(row.get("total_pnl") or 0)
|
||||
if pnl <= 0 and not args.allow_non_positive_pnl:
|
||||
print(f"⚠️ total_pnl={pnl} ≤ 0 → 중단. 적용하려면 --allow-non-positive-pnl")
|
||||
return 6
|
||||
tf = int(data.get("tf") or 60)
|
||||
patch = _apply_updow_row(row, tf=tf)
|
||||
if not patch:
|
||||
print("❌ apply_cfg 가 비어 있습니다.")
|
||||
return 7
|
||||
code_h = str(data.get("code") or "").strip()
|
||||
if args.dry_run:
|
||||
if args.env:
|
||||
print("[dry-run] env patch:", json.dumps(patch, ensure_ascii=False, indent=2))
|
||||
if code_h:
|
||||
_apply_updow_stock(code_h, row, tf=tf, dry_run=True)
|
||||
return 0
|
||||
if args.env:
|
||||
db = TradeDB()
|
||||
try:
|
||||
latest = db.get_latest_env()
|
||||
snap = dict(latest["snapshot"]) if latest else {}
|
||||
snap.update(patch)
|
||||
eid = db.insert_env_snapshot(snap)
|
||||
print(f"✅ UPDOW env_config INSERT id={eid} keys={list(patch.keys())}")
|
||||
finally:
|
||||
db.close()
|
||||
if code_h:
|
||||
_apply_updow_stock(code_h, row, tf=tf, dry_run=False)
|
||||
else:
|
||||
print("⚠️ JSON 에 code 없음 → updow_stock_config 건너뜀")
|
||||
return 0
|
||||
|
||||
# ── top[] 기반 (MOMENTUM / SCALP / BREAKOUT) ─────────────────
|
||||
top = data.get("top") or []
|
||||
if not top:
|
||||
print("❌ JSON 에 top 배열이 없습니다.")
|
||||
return 8
|
||||
if rank > len(top):
|
||||
print(f"❌ rank 범위 초과 (1~{len(top)})")
|
||||
return 5
|
||||
|
||||
item = top[rank - 1]
|
||||
pnl = int(item.get("total_pnl") or 0)
|
||||
if pnl <= 0 and not args.allow_non_positive_pnl:
|
||||
print(f"⚠️ total_pnl={pnl} ≤ 0 → 중단. 적용하려면 --allow-non-positive-pnl")
|
||||
return 6
|
||||
|
||||
patch: Dict[str, str] = {}
|
||||
|
||||
if strategy == "MOMENTUM":
|
||||
merged = merge_param_search_apply_source(item, data)
|
||||
patch = _patch_from_momentum_merged(merged)
|
||||
patch.update(portfolio_env_patch("MOMENTUM", merged))
|
||||
patch.update(session_env_patch("MOMENTUM", merged))
|
||||
|
||||
elif strategy == "BREAKOUT":
|
||||
merged = merge_param_search_apply_source(item, data)
|
||||
patch = _patch_from_breakout_merged(merged)
|
||||
patch.update(portfolio_env_patch("BREAKOUT", merged))
|
||||
patch.update(session_env_patch("BREAKOUT", merged))
|
||||
|
||||
elif strategy == "SCALP":
|
||||
merged = merge_param_search_apply_source(item, data)
|
||||
ds = item.get("db_snapshot")
|
||||
if isinstance(ds, dict) and ds:
|
||||
patch = {str(k): str(v) for k, v in ds.items() if v not in (None, "")}
|
||||
patch.update(portfolio_env_patch("SCALP", merged))
|
||||
else:
|
||||
try:
|
||||
import param_search_scalping as pss # noqa: WPS433
|
||||
|
||||
patch = pss._params_to_db_snapshot(merged)
|
||||
except Exception as e:
|
||||
print(f"❌ SCALP 스냅샷 생성 실패: {e}")
|
||||
return 9
|
||||
|
||||
if not patch:
|
||||
print("❌ 적용할 패치가 비어 있습니다.")
|
||||
return 10
|
||||
|
||||
meta_line = (
|
||||
f" 기간={data.get('start')}~{data.get('end')} mode={data.get('mode')} "
|
||||
f"pnl={item.get('total_pnl')} win={item.get('win_rate')}% trades={item.get('total_trades')}"
|
||||
)
|
||||
print(meta_line)
|
||||
|
||||
if args.dry_run:
|
||||
print("[dry-run] patch keys:", len(patch))
|
||||
print(json.dumps(patch, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
eid = apply_env_patch(patch)
|
||||
if eid is None:
|
||||
print("❌ insert_env_snapshot 실패")
|
||||
return 11
|
||||
print(f"✅ config_* + env_config INSERT id={eid} 갱신 키 수={len(patch)}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user