feat: 새로운 안전 규칙 및 최적화 적용을 통한 트레이딩 시스템 개선
변경 사항 (Changes): 구문 오류(Syntax error) 및 토큰 낭비를 방지하기 위해 에이전트 쉘(Agent shell)과 파이썬 코드 스니펫에 다수의 신규 안전 규칙(Safety rules)을 추가함. 스키마 검증 및 적절한 SQL 포맷팅을 보장하기 위해 임시(Ad-hoc) 데이터베이스 쿼리 작성 가이드라인을 도입함. 코드 수정 후 UI 기능이 정상 작동하는지 확인하기 위해, 백테스트 웹 서비스 재시작 및 브라우저 검증에 대한 새로운 규칙을 구현함. 시스템 전반의 무결성(Integrity)을 유지하기 위해 실전 매매(Live trading), 웹 백테스팅, 파라미터 탐색(Parameter searches) 간의 일관성 검사(Consistency checks) 체계를 확립함. 기대 효과 (Impact): 이러한 개선 사항들은 트레이딩 시스템의 견고성(Robustness)과 신뢰성을 향상시키며, 에러 발생을 최소화하고 다양한 시스템 컴포넌트 간의 원활한 상호작용을 보장함.
This commit is contained in:
@@ -42,7 +42,7 @@ for _p in (ROOT, HERE):
|
||||
from database import TradeDB # noqa: E402
|
||||
from kis_trader.backtest.backtest_portfolio_common import ( # noqa: E402
|
||||
merge_param_search_apply_source,
|
||||
portfolio_env_patch,
|
||||
strip_portfolio_keys_from_apply_patch,
|
||||
session_env_patch,
|
||||
)
|
||||
|
||||
@@ -125,73 +125,52 @@ def _patch_from_momentum_merged(m: Dict[str, Any]) -> Dict[str, str]:
|
||||
|
||||
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)))
|
||||
|
||||
# ※ 모멘텀 apply 는 MOMENTUM_* 만 기록. SCALP_* 를 덮으면 스캘핑 실매/웹이 오염되고,
|
||||
# 엔진은 MOMENTUM_COOLDOWN_SEC 등을 읽는데 SCALP_COOLDOWN 만 써서 apply 가 무효가 된다.
|
||||
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))
|
||||
patch["MOMENTUM_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
|
||||
patch["MOMENTUM_HIGH_CHASE_THR"] = str(ratio_hc)
|
||||
|
||||
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
|
||||
patch["MOMENTUM_MAX_DAILY_CHG"] = str(gf("max_daily_chg", 20.0))
|
||||
|
||||
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"))
|
||||
patch["MOMENTUM_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"]))
|
||||
@@ -200,32 +179,39 @@ def _patch_from_momentum_merged(m: Dict[str, Any]) -> Dict[str, str]:
|
||||
|
||||
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
|
||||
patch["MOMENTUM_TP_MAX_PCT"] = str(abs(float(x)) / 100.0)
|
||||
|
||||
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
|
||||
patch["MOMENTUM_SHOULDER_MIN_HIGH_PCT"] = str(abs(float(x)) / 100.0)
|
||||
|
||||
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
|
||||
patch["MOMENTUM_SHOULDER_CUT_PCT"] = str(abs(float(x)) / 100.0)
|
||||
|
||||
# 전용 트레일(momentum_engine) — UI 퍼센트(0.5) → 엔진 비율(0.005) 저장.
|
||||
# (기존엔 trail_trigger/stop=SCALP_ATR_*만 기록돼 MOMENTUM_TRAIL_PCT가 0으로 남던 버그 수정)
|
||||
x = m.get("trail_pct")
|
||||
# 전용 트레일(momentum_engine) — UI 퍼센트 → MOMENTUM_TRAIL_* 비율.
|
||||
# trail_pct/arm 우선. 레거시 trail_trigger/stop 만 있으면 과거 Optuna와 같이 OFF(0).
|
||||
has_trail_pct = "trail_pct" in m and m.get("trail_pct") not in (None, "")
|
||||
has_trail_arm = "trail_arm_pct" in m and m.get("trail_arm_pct") not in (None, "")
|
||||
has_legacy_trail = (
|
||||
("trail_trigger" in m and m.get("trail_trigger") not in (None, ""))
|
||||
or ("trail_stop" in m and m.get("trail_stop") not in (None, ""))
|
||||
)
|
||||
if has_trail_pct:
|
||||
patch["MOMENTUM_TRAIL_PCT"] = str(abs(float(m["trail_pct"])) / 100.0)
|
||||
elif has_legacy_trail and not has_trail_arm:
|
||||
patch["MOMENTUM_TRAIL_PCT"] = "0"
|
||||
if has_trail_arm:
|
||||
patch["MOMENTUM_TRAIL_ARM_PCT"] = str(abs(float(m["trail_arm_pct"])) / 100.0)
|
||||
elif has_legacy_trail and not has_trail_pct:
|
||||
patch["MOMENTUM_TRAIL_ARM_PCT"] = "0"
|
||||
|
||||
# 래칫·최대보유 — exit Optuna 1위 적용 시 누락되지 않도록 (BREAKOUT_RATCHET_TIERS 와 동일 계열)
|
||||
if "ratchet_tiers" in m:
|
||||
patch["MOMENTUM_RATCHET_TIERS"] = str(m.get("ratchet_tiers") or "").strip()
|
||||
x = m.get("max_hold_bars")
|
||||
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)
|
||||
patch["MOMENTUM_MAX_HOLD_BARS"] = str(int(float(x)))
|
||||
|
||||
if "use_ema_filter" in m:
|
||||
patch["MOMENTUM_USE_EMA_FILTER"] = _env_bool_10(m.get("use_ema_filter"))
|
||||
@@ -258,6 +244,9 @@ def _patch_from_momentum_merged(m: Dict[str, Any]) -> Dict[str, str]:
|
||||
if x not in (None, ""):
|
||||
patch["MOMENTUM_EMA_SLOW_PERIOD"] = str(int(float(x)))
|
||||
|
||||
from kis_trader.engine.orderbook_env import orderbook_params_to_env_patch
|
||||
patch.update(orderbook_params_to_env_patch("MOMENTUM", m))
|
||||
|
||||
return patch
|
||||
|
||||
|
||||
@@ -362,7 +351,8 @@ def _patch_from_breakout_merged(m: Dict[str, Any]) -> Dict[str, str]:
|
||||
|
||||
x = gv("max_daily")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_MAX_DAILY"] = str(int(float(x)))
|
||||
# 일일횟수는 포트폴리오성 운영값 — Optuna apply 가 1로 덮지 않음(실매·웹에서 별도)
|
||||
pass
|
||||
x = gv("cooldown_min")
|
||||
if x is not None:
|
||||
patch["BREAKOUT_COOLDOWN_SEC"] = str(int(float(x) * 60))
|
||||
@@ -412,6 +402,9 @@ def _patch_from_breakout_merged(m: Dict[str, Any]) -> Dict[str, str]:
|
||||
if x is not None:
|
||||
patch["BREAKOUT_EMA_SLOW_PERIOD"] = str(int(float(x)))
|
||||
|
||||
from kis_trader.engine.orderbook_env import orderbook_params_to_env_patch
|
||||
patch.update(orderbook_params_to_env_patch("BREAKOUT", m))
|
||||
|
||||
return patch
|
||||
|
||||
|
||||
@@ -584,29 +577,30 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
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))
|
||||
patch = strip_portfolio_keys_from_apply_patch(patch, "MOMENTUM")
|
||||
|
||||
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))
|
||||
patch = strip_portfolio_keys_from_apply_patch(patch, "BREAKOUT")
|
||||
|
||||
elif strategy == "SCALP":
|
||||
merged = merge_param_search_apply_source(item, data)
|
||||
ds = item.get("db_snapshot")
|
||||
if isinstance(ds, dict) and ds:
|
||||
# 구 JSON db_snapshot 에 포트폴리오(600만 등)가 있어도 apply 시 제외
|
||||
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
|
||||
from kis_trader.backtest 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
|
||||
patch = strip_portfolio_keys_from_apply_patch(patch, "SCALP")
|
||||
|
||||
if not patch:
|
||||
print("❌ 적용할 패치가 비어 있습니다.")
|
||||
|
||||
Reference in New Issue
Block a user