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:
2026-07-06 01:27:00 +09:00
parent d8ba01afa4
commit 61c72a8a4c
171 changed files with 176914 additions and 7329 deletions

View File

@@ -59,11 +59,11 @@ DEFAULT_V1_CONFIG: Dict = {
"take_profit_pct": 15.0, # 평단가 대비 익절 %
"stop_loss_pct": 10.0, # 평단가 대비 손절 %
# ── 투자금 / 분할 비율 ────────────────────────────────────────────────────
# ── 투자금 / 분할 비율 (holding_bot·웹 UI와 동일 — 0~100 퍼센트) ─────────
"slot_money": 3_000_000.0,
"buy1_ratio": 0.4, # 1단계 투자금 비율 (40%)
"buy2_ratio": 0.35, # 2단계 투자금 비율 (35%)
"buy3_ratio": 0.25, # 3단계 투자금 비율 (25%)
"buy1_ratio": 40.0, # 1단계 투자금 비율 (%)
"buy2_ratio": 35.0, # 2단계 투자금 비율 (%)
"buy3_ratio": 25.0, # 3단계 투자금 비율 (%)
# ── 비용 ────────────────────────────────────────────────────────────────
"fee_rate": 0.0015, # 수수료율 (편도)
@@ -94,9 +94,10 @@ def run_backtest_v1(candles: List[Dict], cfg: Dict) -> Dict:
tp_pct = float(cfg.get("take_profit_pct",DEFAULT_V1_CONFIG["take_profit_pct"]))
sl_pct = float(cfg.get("stop_loss_pct", DEFAULT_V1_CONFIG["stop_loss_pct"]))
slot_money = float(cfg.get("slot_money", DEFAULT_V1_CONFIG["slot_money"]))
buy1_r = float(cfg.get("buy1_ratio", DEFAULT_V1_CONFIG["buy1_ratio"]))
buy2_r = float(cfg.get("buy2_ratio", DEFAULT_V1_CONFIG["buy2_ratio"]))
buy3_r = float(cfg.get("buy3_ratio", DEFAULT_V1_CONFIG["buy3_ratio"]))
# 웹·DB는 30=30% 형식 — holding_bot.run_backtest 와 동일하게 /100
buy1_r = float(cfg.get("buy1_ratio", DEFAULT_V1_CONFIG["buy1_ratio"])) / 100.0
buy2_r = float(cfg.get("buy2_ratio", DEFAULT_V1_CONFIG["buy2_ratio"])) / 100.0
buy3_r = float(cfg.get("buy3_ratio", DEFAULT_V1_CONFIG["buy3_ratio"])) / 100.0
fee_rate = float(cfg.get("fee_rate", DEFAULT_V1_CONFIG["fee_rate"]))
sell_tax = float(cfg.get("sell_tax", DEFAULT_V1_CONFIG["sell_tax"]))
ath_drop_min = float(cfg.get("ath_drop_min_pct", DEFAULT_V1_CONFIG["ath_drop_min_pct"]))
@@ -280,17 +281,21 @@ def run_backtest_v1(candles: List[Dict], cfg: Dict) -> Dict:
peak_eq = max(peak_eq, run_pnl)
mdd = max(mdd, peak_eq - run_pnl)
# Buy & Hold 비교 (첫 진입 시점 기준)
bnh_pct = 0.0; bnh_pnl = 0.0; bot_pct = 0.0
if candles and slot_money > 0:
first_p = float(candles[start_i]["open"])
last_p = float(candles[-1]["close"])
if first_p > 0:
bnh_qty = max(1, int(slot_money / first_p))
bnh_pnl = round((last_p - first_p) * bnh_qty)
bnh_pct = round((last_p - first_p) / first_p * 100, 2)
# Buy & Hold 비교 — 전구간·시뮬시작 동일 구간 모두 종가 기준 (추세BT와 동일 축)
total_pnl = sum(t["pnl"] for t in trades)
bot_pct = round(total_pnl / slot_money * 100, 2) if slot_money > 0 else 0.0
bnh_pct = bnh_pnl = bnh_aligned_pct = bnh_aligned_pnl = 0.0
alpha_pct = alpha_aligned_pct = 0.0
if candles and slot_money > 0 and closes[0] > 0:
last_c = float(candles[-1]["close"])
bnh_pct = round((last_c - closes[0]) / closes[0] * 100, 2)
bnh_pnl = round(slot_money * bnh_pct / 100)
c0_al = closes[start_i] if start_i < len(closes) else closes[0]
if c0_al > 0:
bnh_aligned_pct = round((last_c - c0_al) / c0_al * 100, 2)
bnh_aligned_pnl = round(slot_money * bnh_aligned_pct / 100)
alpha_pct = round(bot_pct - bnh_pct, 2)
alpha_aligned_pct = round(bot_pct - bnh_aligned_pct, 2)
reason_dist: Dict[str, int] = {}
for t in trades:
@@ -312,6 +317,10 @@ def run_backtest_v1(candles: List[Dict], cfg: Dict) -> Dict:
"bnh_pct": bnh_pct,
"bnh_pnl": bnh_pnl,
"bot_pct": bot_pct,
"alpha_pct": alpha_pct,
"bnh_aligned_pct": bnh_aligned_pct,
"bnh_aligned_pnl": bnh_aligned_pnl,
"alpha_aligned_pct": alpha_aligned_pct,
},
"equity": equity[-200:],
"trades": trades[-200:],
@@ -369,7 +378,7 @@ def run_param_search_v1(
if "error" in res:
continue
s = res.get("summary", {})
if s.get("total_trades", 0) < min_trades:
if min_trades > 0 and s.get("total_trades", 0) < min_trades:
continue
results.append({
"params": {k: cfg[k] for k in keys},
@@ -399,7 +408,7 @@ def main():
parser.add_argument("--start", default=year_ago, help="시작일 YYYY-MM-DD")
parser.add_argument("--end", default=today, help="종료일 YYYY-MM-DD")
parser.add_argument("--search", action="store_true", help="파라미터 탐색 모드")
parser.add_argument("--min_trades", default=2, type=int, help="탐색 최소 거래 수")
parser.add_argument("--min_trades", default=1, type=int, help="탐색 최소 거래 수 (0=제한 없음)")
parser.add_argument("--top", default=20, type=int, help="탐색 결과 상위 N개")
parser.add_argument("--rsi_buy1", default=None, type=float)
parser.add_argument("--rsi_buy2", default=None, type=float)