fix: Rust 파라미터 매핑 오류 및 순차 스터디 라벨 누락 수정
- tail_engine.py, breakout_engine.py: Rust엔진 연동 시 파라미터 키(sl_pct, tp_pct 등) 불일치 오류 수정 - optuna_mode_refine_runner.py: 다중 모드(seq) 1·2차 스터디명 생성 시 진입/돌파 모드(extra) 정보 추가하여 라벨 유실 방지
This commit is contained in:
@@ -238,7 +238,15 @@ def run_mode_refine(
|
||||
env.pop("OPTUNA_GRID_NARROW_JSON", None)
|
||||
|
||||
p1_study_arg = str(phase1_study or "").strip()
|
||||
p1_study = f"{strategy}_{mode}_refine1_{start.replace('-', '')}_{end.replace('-', '')}_{ts}"
|
||||
|
||||
extra = ""
|
||||
if strategy == "tail" and entry_mode:
|
||||
extra = f"_{entry_mode}"
|
||||
elif strategy == "breakout":
|
||||
from kis_trader.backtest.optuna_breakout_tpe_space import breakout_tpe_study_extra
|
||||
extra = f"_{breakout_tpe_study_extra(sl_mode or 'fixed', ob_mode or 'off')}"
|
||||
|
||||
p1_study = f"{strategy}{extra}_{mode}_refine1_{start.replace('-', '')}_{end.replace('-', '')}_{ts}"
|
||||
p1_log = ROOT / "logs" / f"optuna_refine1_{job_id}.log"
|
||||
p1_json = str(phase1_json or "").strip()
|
||||
|
||||
@@ -331,7 +339,7 @@ def run_mode_refine(
|
||||
state["narrow_path"] = str(narrow_path)
|
||||
|
||||
p2_trials = resolve_refine_phase2_trials(trials)
|
||||
p2_study = f"{strategy}_{mode}_refine2_{start.replace('-', '')}_{end.replace('-', '')}_{ts}"
|
||||
p2_study = f"{strategy}{extra}_{mode}_refine2_{start.replace('-', '')}_{end.replace('-', '')}_{ts}"
|
||||
p2_log = ROOT / "logs" / f"optuna_refine2_{job_id}.log"
|
||||
env2 = dict(env)
|
||||
env2["OPTUNA_GRID_NARROW_JSON"] = str(narrow_path)
|
||||
|
||||
108
kis_trader/engine/breakout_engine.py
Normal file
108
kis_trader/engine/breakout_engine.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
try:
|
||||
import kis_rust_core
|
||||
except ImportError:
|
||||
kis_rust_core = None
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def run_breakout_backtest_rust_experimental(
|
||||
codes_candles: Dict[str, List[Dict]],
|
||||
params: Dict[str, Any],
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
Rust 엔진을 이용한 Breakout (돌파매매) 고속 백테스트 브릿지.
|
||||
"""
|
||||
if kis_rust_core is None:
|
||||
logger.warning("kis_rust_core is not installed or imported. Falling back to empty trades.")
|
||||
return []
|
||||
|
||||
# 파라미터 파싱
|
||||
lookback_min = int(params.get("lookback_min", 1))
|
||||
vol_window = int(params.get("vol_window", 7))
|
||||
vol_mult = float(params.get("vol_mult", 0.0) or 0.0)
|
||||
prev_chg_min = float(params.get("prev_chg_min", 1.0))
|
||||
prev_chg_max = float(params.get("prev_chg_max", 10.0))
|
||||
max_daily_chg = float(params.get("max_daily_chg", 15.0))
|
||||
min_price = float(params.get("min_price", 1000.0))
|
||||
min_bar_trade_value_krw = float(params.get("min_bar_trade_value_krw", 0.0) or 0.0)
|
||||
min_turnover_1m_pct = float(params.get("min_turnover_1m_pct", 0.0) or 0.0)
|
||||
share_denom = float(params.get("share_denom", 0.0) or 0.0)
|
||||
confirm_margin_pct = float(params.get("confirm_margin_pct", 0.0) or 0.0)
|
||||
body_min_pct = float(params.get("body_min_pct", 0.0) or 0.0)
|
||||
use_ema_filter = bool(params.get("use_ema_filter", False))
|
||||
ema_fast_period = int(params.get("ema_fast_period", 9))
|
||||
ema_slow_period = int(params.get("ema_slow_period", 21))
|
||||
time_start_hm = int(params.get("time_start_hm", 900))
|
||||
time_end_hm = int(params.get("time_end_hm", 1030))
|
||||
sl_pct = abs(float(params.get("sl_pct", params.get("stop_loss_pct", -0.02))))
|
||||
tp_pct = float(params.get("tp_pct", params.get("take_profit_pct", 0.05)))
|
||||
trail_pct = float(params.get("trail_pct", 0.015))
|
||||
trail_arm_pct = float(params.get("trail_arm_pct", 0.0) or 0.0)
|
||||
shoulder_min_high = float(params.get("shoulder_min_high_pct", params.get("shoulder_min_high", 0.02)))
|
||||
shoulder_cut_pct = float(params.get("shoulder_cut_pct", 0.01))
|
||||
max_hold_bars = int(params.get("max_hold_bars", 0) or 0)
|
||||
cooldown_min = float(params.get("cooldown_min", 30))
|
||||
max_daily = int(params.get("max_daily", 1))
|
||||
skip_hts_scan_dupes = bool(params.get("skip_hts_scan_dupes", False))
|
||||
atr_period = int(params.get("atr_period", 14) or 14)
|
||||
|
||||
# 파라미터 구조체 생성
|
||||
rust_params = kis_rust_core.BreakoutParams(
|
||||
lookback_min, vol_window, vol_mult, prev_chg_min, prev_chg_max,
|
||||
max_daily_chg, min_price, min_bar_trade_value_krw, min_turnover_1m_pct,
|
||||
share_denom, confirm_margin_pct, body_min_pct, use_ema_filter,
|
||||
ema_fast_period, ema_slow_period, time_start_hm, time_end_hm,
|
||||
sl_pct, tp_pct, trail_pct, trail_arm_pct, shoulder_min_high,
|
||||
shoulder_cut_pct, max_hold_bars, cooldown_min, max_daily, skip_hts_scan_dupes,
|
||||
atr_period
|
||||
)
|
||||
|
||||
all_trades = []
|
||||
|
||||
for code, candles_dict in codes_candles.items():
|
||||
if not candles_dict:
|
||||
continue
|
||||
|
||||
rust_candles = []
|
||||
for c in candles_dict:
|
||||
rc = kis_rust_core.CandleData(
|
||||
c.get("candle_time", ""),
|
||||
float(c.get("open", 0)),
|
||||
float(c.get("high", 0)),
|
||||
float(c.get("low", 0)),
|
||||
float(c.get("close", 0)),
|
||||
float(c.get("volume", 0)),
|
||||
float(c.get("rsi", 0.0)),
|
||||
)
|
||||
rust_candles.append(rc)
|
||||
|
||||
try:
|
||||
trades = kis_rust_core.run_breakout_backtest_fast(code, rust_candles, rust_params)
|
||||
for t in trades:
|
||||
all_trades.append({
|
||||
"code": t.code,
|
||||
"buy_time": t.buy_time,
|
||||
"sell_time": t.sell_time,
|
||||
"buy_price": t.buy_price,
|
||||
"sell_price": t.sell_price,
|
||||
"profit_rate": t.pnl_pct,
|
||||
"sell_reason": t.reason,
|
||||
"qty": 1, # 단순화를 위해 1로 고정, 추후 예산 로직 반영 가능
|
||||
"pnl": 0, # 단순화를 위해 0,
|
||||
# 부가 정보 기록
|
||||
"entry_features": {
|
||||
"resistance": t.resistance,
|
||||
"vol_ratio": t.vol_ratio,
|
||||
"prev_chg": t.prev_chg,
|
||||
},
|
||||
"atr_entry": t.atr_entry,
|
||||
"max_price": getattr(t, "max_price", t.buy_price),
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Rust breakout engine error for {code}: {e}")
|
||||
|
||||
all_trades.sort(key=lambda x: x["sell_time"])
|
||||
return all_trades
|
||||
@@ -2519,6 +2519,89 @@ def run_tail_backtest_portfolio(
|
||||
return all_trades
|
||||
|
||||
|
||||
def run_tail_backtest_rust_experimental(
|
||||
codes_candles: Dict[str, List[Dict]],
|
||||
params: Dict[str, Any]
|
||||
) -> List[Dict]:
|
||||
"""꼬리잡기 매매 Rust 엔진 실험용 브릿지"""
|
||||
try:
|
||||
import kis_rust_core
|
||||
except ImportError:
|
||||
raise RuntimeError("kis_rust_core module is not available")
|
||||
|
||||
def get_float(k, default=0.0):
|
||||
try: return float(params.get(k, default))
|
||||
except: return float(default)
|
||||
|
||||
def get_int(k, default=0):
|
||||
try: return int(params.get(k, default))
|
||||
except: return int(default)
|
||||
|
||||
def get_bool(k, default=False):
|
||||
v = params.get(k, default)
|
||||
if isinstance(v, bool): return v
|
||||
s = str(v).lower()
|
||||
if s in ("1", "true", "yes", "t", "y"): return True
|
||||
return False
|
||||
|
||||
rust_params = kis_rust_core.TailParams(
|
||||
get_int("time_start_hm", 930),
|
||||
get_int("time_end_hm", 1500),
|
||||
get_float("cooldown_min", 15.0),
|
||||
get_int("max_daily", 3),
|
||||
get_float("min_price", 1000.0),
|
||||
get_float("rsi_threshold", 78.0),
|
||||
get_float("min_drop_rate", 0.03),
|
||||
get_float("min_recovery_ratio", 0.5),
|
||||
get_float("tail_pct_min", 0.003),
|
||||
get_bool("skip_hts_scan_dupes", True),
|
||||
abs(get_float("sl_pct", 0.03)),
|
||||
get_float("tp_pct", 0.05),
|
||||
get_float("trail_pct", 0.0),
|
||||
get_float("trail_arm_pct", 0.0),
|
||||
get_float("shoulder_min_high", 0.005),
|
||||
get_float("shoulder_cut_pct", 0.003),
|
||||
get_int("max_hold_bars", 0),
|
||||
get_float("max_loss_krw", 200000.0),
|
||||
get_float("min_drop_pct_for_loss_cut", 0.015),
|
||||
)
|
||||
|
||||
all_trades = []
|
||||
|
||||
for code, candles_dict in codes_candles.items():
|
||||
rust_candles = []
|
||||
for c in candles_dict:
|
||||
rc = kis_rust_core.CandleData(
|
||||
c.get("candle_time", ""),
|
||||
float(c.get("open", 0)),
|
||||
float(c.get("high", 0)),
|
||||
float(c.get("low", 0)),
|
||||
float(c.get("close", 0)),
|
||||
float(c.get("volume", 0)),
|
||||
float(c.get("rsi", 0.0)),
|
||||
)
|
||||
rust_candles.append(rc)
|
||||
|
||||
trades = kis_rust_core.run_tail_backtest_fast(code, rust_candles, rust_params)
|
||||
for t in trades:
|
||||
all_trades.append({
|
||||
"code": t.code,
|
||||
"buy_time": t.buy_time,
|
||||
"sell_time": t.sell_time,
|
||||
"buy_price": t.buy_price,
|
||||
"sell_price": t.sell_price,
|
||||
"profit_rate": t.pnl_pct,
|
||||
"qty": 1,
|
||||
"pnl": 0,
|
||||
"sell_reason": t.reason,
|
||||
"max_price": t.max_price,
|
||||
"rsi_entry": t.rsi_entry,
|
||||
"strategy": "SHORT"
|
||||
})
|
||||
|
||||
return all_trades
|
||||
|
||||
|
||||
def run_tail_backtest(
|
||||
candles_by_code: Dict[str, List[Dict]],
|
||||
params: Dict[str, Any],
|
||||
@@ -2533,6 +2616,9 @@ def run_tail_backtest(
|
||||
기본: ``portfolio_mode=true`` → 시각순 포트폴리오 (실매 MAX_STOCKS·총한도·slot_money).
|
||||
``portfolio_mode=false`` → 레거시 종목별 독립 루프.
|
||||
"""
|
||||
if params.get("use_rust", False):
|
||||
return run_tail_backtest_rust_experimental(candles_by_code, params)
|
||||
|
||||
if _to_bool(params.get("portfolio_mode"), True):
|
||||
return run_tail_backtest_portfolio(
|
||||
candles_by_code, params, universe_by_slot,
|
||||
|
||||
Reference in New Issue
Block a user