Files
kis_bot/kis_trader/scripts/verify_three_paths.py
Your Name fc27e726f9 feat: 새로운 안전 규칙 및 최적화 적용을 통한 트레이딩 시스템 개선
변경 사항 (Changes):

구문 오류(Syntax error) 및 토큰 낭비를 방지하기 위해 에이전트 쉘(Agent shell)과 파이썬 코드 스니펫에 다수의 신규 안전 규칙(Safety rules)을 추가함.

스키마 검증 및 적절한 SQL 포맷팅을 보장하기 위해 임시(Ad-hoc) 데이터베이스 쿼리 작성 가이드라인을 도입함.

코드 수정 후 UI 기능이 정상 작동하는지 확인하기 위해, 백테스트 웹 서비스 재시작 및 브라우저 검증에 대한 새로운 규칙을 구현함.

시스템 전반의 무결성(Integrity)을 유지하기 위해 실전 매매(Live trading), 웹 백테스팅, 파라미터 탐색(Parameter searches) 간의 일관성 검사(Consistency checks) 체계를 확립함.

기대 효과 (Impact):

이러한 개선 사항들은 트레이딩 시스템의 견고성(Robustness)과 신뢰성을 향상시키며, 에러 발생을 최소화하고 다양한 시스템 컴포넌트 간의 원활한 상호작용을 보장함.
2026-07-17 01:09:09 +09:00

232 lines
11 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
verify_three_paths.py — 실매 / param_search / 웹백테 가 '똑같은 엔진 파라미터'
도는지 검증한다.
목적
----
JS(웹) 입력값은 표시단위(%)로 받아 쿼리스트링으로 전송되고, 파이썬(api_backtest_*)
에서 ÷100 등으로 엔진 비율(ratio)로 되돌린다. 이 왕복(ratio→표시→ratio)이
손실 없이 카논(실매·param_search 가 쓰는 get_*_defaults_from_db) 과 100% 일치하는지
수치로 확인한다. (소수점/정수 변환 차이 적발)
검증 구조
---------
- 실매(live) : 전략 객체가 get_*_defaults_from_db() 비율값을 그대로 사용.
- param_search: base = get_*_defaults_from_db() (코드상 동일 함수 → 자동 일치).
- 웹백테(web) : _*_ui_defaults_from_db() (표시%) → (JS는 숫자 그대로 통과)
→ api_backtest_* 변환(÷100) → 엔진 비율.
따라서 'web 왕복 후 비율' == 'canonical 비율' 이면 세 경로가 동일하다.
이 스크립트는 외부 서버/데이터 없이 변환만 재현해 비교한다.
"""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
# 허용 오차 — 부동소수 반올림(웹 표시 round(x*100,3)) 으로 생길 수 있는 미세 오차
TOL = 1e-9
def _fmt(v):
if isinstance(v, float):
return f"{v:.10g}"
return str(v)
def _cmp_rows(rows):
"""rows: [(name, canonical, web, unit)] → 출력 + 불일치 수 반환."""
bad = 0
print(f" {'필드':28} {'canonical(실매/파서치)':>22} {'web 왕복후':>16} 판정")
print(" " + "-" * 78)
for name, can, web, unit in rows:
if isinstance(can, (int, float)) and isinstance(web, (int, float)):
ok = abs(float(can) - float(web)) <= TOL
else:
ok = str(can) == str(web)
mark = "OK " if ok else "❌MISMATCH"
if not ok:
bad += 1
print(f" {name:28} {_fmt(can):>22} {_fmt(web):>16} {mark} {unit}")
return bad
# ──────────────────────────────────────────────────────────────────────────
# 공통: 웹 표시% → 엔진 비율 (api_backtest_scalping 의 sl/tp 변환과 동일)
# ──────────────────────────────────────────────────────────────────────────
def _ui_pct_to_ratio(ui_pct) -> float:
"""웹 입력(%) → 엔진 비율. api_backtest_scalping: float(x)/100 후 abs."""
return abs(float(ui_pct) / 100.0)
def verify_momentum() -> int:
import backtest_web as bw
import kis_trader.engine.momentum_engine as me
import scalping_engine as se # noqa: F401 (웹이 쓰는 베이스 로더)
print("\n=== 모멘텀 (MOMENTUM) ===")
can = me.get_momentum_defaults_from_db() # 실매 + param_search base (비율)
ui = bw._momentum_ui_defaults_from_db(can) # 웹 표시값 (%)
# 웹 → 엔진 비율 재현 (api_backtest_scalping 변환)
rows = [
("sl_pct(손절)", can["sl_pct"], _ui_pct_to_ratio(ui["sl_pct"]), "비율"),
("tp_pct(익절)", can["tp_pct"], _ui_pct_to_ratio(ui["tp_pct"]), "비율"),
("tp_max_pct(익절상한)", can["tp_max_pct"], _ui_pct_to_ratio(ui["tp_max_pct"]), "비율"),
("shoulder_min_high", can["shoulder_min_high"], _ui_pct_to_ratio(ui["shoulder_min_high"]), "비율"),
("shoulder_cut_pct", can["shoulder_cut_pct"], _ui_pct_to_ratio(ui["shoulder_cut_pct"]), "비율"),
# 정수/그대로 통과 필드
("mom_rsi_min", can["mom_rsi_min"], float(ui["mom_rsi_min"]), "그대로"),
("mom_rsi_max", can["mom_rsi_max"], float(ui["mom_rsi_max"]), "그대로"),
("mom_vol_mult", can["mom_vol_mult"], float(ui["mom_vol_mult"]), "그대로"),
("mom_vol_win", can["mom_vol_win"], int(float(ui["mom_vol_win"])), "정수"),
("max_daily", can["max_daily"], int(float(ui["max_daily"])), "정수"),
]
return _cmp_rows(rows)
def verify_breakout() -> int:
import backtest_web as bw
from kis_trader.strategies.breakout import breakout_ui_to_engine_params
print("\n=== 돌파 (BREAKOUT) ===")
# 웹과 param_search 는 둘 다 breakout_ui_to_engine_params 사용 (동일 함수).
ui = bw._bo_defaults_from_db()
web_engine = bw._bo_ui_to_engine_params(ui) # 웹 경로 엔진값
ps_engine = breakout_ui_to_engine_params(dict(ui)) # param_search 경로 (같은 함수)
# 실매(live) 가 읽는 DB 비율과도 일치하는지 — breakout strategy 기본 키
from kis_trader.utils.env import get_strategy_env_dict
env = get_strategy_env_dict("BREAKOUT")
def env_ratio(key, default):
v = env.get(key)
if v in (None, "", "None"):
return float(default)
return abs(float(v))
keys = [
("sl_pct", "stop_loss_pct"),
("tp_pct", "take_profit_pct"),
("trail_pct", "trail_pct"),
("shoulder_min_high_pct", "shoulder_min_high_pct"),
("shoulder_cut_pct", "shoulder_cut_pct"),
]
bad = 0
print(" [A] 웹 vs param_search (동일 함수여야 100% 일치)")
web_keys = sorted(set(web_engine) & set(ps_engine))
for k in web_keys:
a, b = web_engine.get(k), ps_engine.get(k)
if isinstance(a, (int, float)) and isinstance(b, (int, float)):
if abs(float(a) - float(b)) > TOL:
print(f"{k}: web={a} ps={b}")
bad += 1
elif str(a) != str(b):
print(f"{k}: web={a} ps={b}")
bad += 1
if bad == 0:
print(f" OK — 공통 {len(web_keys)}개 키 전부 일치")
print(" [B] 웹 엔진비율 vs 실매 DB 비율")
rows = []
for eng_key, _ in keys:
if eng_key not in web_engine:
continue
rows.append((eng_key, web_engine[eng_key], web_engine[eng_key], "비율(웹=엔진)"))
# 실제 비교: web_engine 값이 DB 원본 비율과 같은지
rows2 = []
sl_dbf = env_ratio("BREAKOUT_STOP_LOSS_PCT", 0.02)
rows2.append(("BREAKOUT_STOP_LOSS_PCT", sl_dbf, abs(float(web_engine.get("stop_loss_pct", web_engine.get("sl_pct", sl_dbf)))), "비율"))
bad += _cmp_rows(rows2)
return bad
def verify_tail() -> int:
import backtest_web as bw
import kis_trader.engine.tail_engine as te
print("\n=== 꼬리 (TAIL/SHORT) ===")
can = te.get_tail_defaults_from_db() # 실매 + param_search base
ui = bw._tail_ui_defaults_from_db() # 웹 표시값
# 웹 표시(%) → 엔진 비율 재현 후 카논과 비교 (낙폭/손절/익절/어깨 계열)
rows = [
("sl_pct", can.get("sl_pct"), _ui_pct_to_ratio(ui["sl_pct"]), "비율"),
("tp_pct", can.get("tp_pct"), _ui_pct_to_ratio(ui["tp_pct"]), "비율"),
("shoulder_min_high", can.get("shoulder_min_high"), _ui_pct_to_ratio(ui["smin"]), "비율"),
("shoulder_cut_pct", can.get("shoulder_cut_pct"), _ui_pct_to_ratio(ui["scut"]), "비율"),
("rsi_threshold", can.get("rsi_threshold"), float(ui["rsi"]), "그대로"),
("rsi_period", can.get("rsi_period"), int(float(ui["rsi_period"])), "정수"),
("max_daily", can.get("max_daily"), int(float(ui["max_daily"])), "정수"),
("stop_atr_mult", can.get("stop_atr_mult"), float(ui["stop_atr_mult"]), "그대로"),
("target_atr_mult", can.get("target_atr_mult"), float(ui["target_atr_mult"]), "그대로"),
]
return _cmp_rows(rows)
def verify_scalping() -> int:
import backtest_web as bw
import kis_trader.engine.scalping_engine as se
from kis_trader.backtest.param_search_scalping import _fixed_defaults, _ui_to_engine_params
print("\n=== 스캘핑 (SCALP) ===")
can = se.get_scalping_defaults_from_db() # 실매 + param_search base (비율)
ui = bw._scalp_ui_defaults_from_db() # 웹 표시값 (%)
# [A] 웹 표시(%) → ÷100 왕복 == 실매 비율
rows = [
("sl_pct(손절)", can["sl_pct"], _ui_pct_to_ratio(ui["sl_pct"]), "비율"),
("tp_pct(익절)", can["tp_pct"], _ui_pct_to_ratio(ui["tp_pct"]), "비율"),
("tp_max_pct(익절상한)", can["tp_max_pct"], _ui_pct_to_ratio(ui["tp_max_pct"]), "비율"),
("drop_rate(낙폭)", can["drop_rate"], _ui_pct_to_ratio(ui["drop_rate"]), "비율"),
("shoulder_min_high", can["shoulder_min_high"], _ui_pct_to_ratio(ui["shoulder_min_high"]), "비율"),
("shoulder_cut_pct", can["shoulder_cut_pct"], _ui_pct_to_ratio(ui["shoulder_cut_pct"]), "비율"),
("rsi_oversold", can["rsi_oversold"], float(ui["rsi_oversold"]), "그대로"),
("rsi_period", can["rsi_period"], int(float(ui["rsi_period"])), "정수"),
("max_daily", can["max_daily"], int(float(ui["max_daily"])), "정수"),
("use_macd_cross", bool(can.get("use_macd_cross")), bool(ui.get("use_macd_cross")), "bool"),
("skip_hts_scan_dupes", bool(can.get("skip_hts_scan_dupes")),
bool(ui.get("skip_hts_scan_dupes")), "bool"),
]
bad = _cmp_rows(rows)
# [B] param_search _ui_to_engine == 실매 비율
print(" [B] param_search _ui_to_engine vs 실매 비율")
ps_ui = _fixed_defaults()
eng = _ui_to_engine_params(dict(ps_ui))
rows_b = []
for k in (
"sl_pct", "tp_pct", "tp_max_pct", "drop_rate",
"shoulder_min_high", "shoulder_cut_pct",
):
rows_b.append((k, can.get(k), eng.get(k), "비율"))
bad += _cmp_rows(rows_b)
return bad
def main() -> int:
total_bad = 0
for fn in (verify_momentum, verify_breakout, verify_tail, verify_scalping):
try:
total_bad += fn()
except Exception as e: # noqa: BLE001
import traceback
print(f"\n[ERROR] {fn.__name__}: {e}")
traceback.print_exc()
total_bad += 1
print("\n" + "=" * 80)
if total_bad == 0:
print("✅ 검증 통과 — 실매 / param_search / 웹백테 가 동일한 엔진 파라미터로 돕니다.")
else:
print(f"❌ 불일치 {total_bad}건 — 위 MISMATCH 항목을 확인하세요.")
return total_bad
if __name__ == "__main__":
sys.exit(0 if main() == 0 else 1)