Files
kis_bot/kis_trader/backtest/dbband_param_search.py
Hwang 61c72a8a4c 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>
2026-07-06 01:27:00 +09:00

158 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""
kis_trader/backtest/dbband_param_search.py — DBBAND 종목별 Grid Search
======================================================================
실행:
python3 -m kis_trader.backtest.dbband_param_search --symbol QQQM --start 2026-05-01 --end 2026-05-29 --tf 15
python3 -m kis_trader.backtest.dbband_param_search --symbol 069500 --tf 15
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from datetime import datetime, timedelta
from itertools import product
from typing import Any, Dict, List, Optional
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(HERE))
if ROOT not in sys.path:
sys.path.insert(0, ROOT)
import holding_bot as hb
from database import TradeDB
from kis_trader.backtest import dbband_backtest_common as bbc
from kis_trader.engine import dbband_engine as bbe
from kis_trader.engine.dbband_env_keys import params_to_dbband_env_patch
from kis_trader.strategies import dbband_stock_cfg as dsc
def _results_dir() -> str:
d = os.path.join(HERE, "results")
os.makedirs(d, exist_ok=True)
return d
def _grid_axes(snap: Dict[str, Any]) -> Dict[str, List[Any]]:
def _f(k: str, default: str) -> List[float]:
raw = str(snap.get(k) or default)
return [float(x.strip()) for x in raw.split(",") if x.strip()]
return {
"bb_inner_std": _f("DBBAND_GRID_INNER_STD0", "2,2.5") or [2.0, 2.5],
"bb_outer_std": _f("DBBAND_GRID_OUTER_STD0", "2.5,3,3.5") or [2.5, 3.0, 3.5],
"trend_ma_period": [int(x) for x in _f("DBBAND_GRID_TREND_MA0", "100,120,150,200") or [100, 120, 150, 200]],
"stop_loss_pct": _f("DBBAND_GRID_SL0", "1.5,2,2.5") or [1.5, 2.0, 2.5],
"tp_mode": ["opposite_band", "rr"],
"rr_ratio": _f("DBBAND_GRID_RR0", "1.5,2") or [1.5, 2.0],
}
def run_search_for_code(
db: Any,
code: str,
start_date: str,
end_date: str,
tf: int,
*,
base_cfg: Optional[Dict[str, Any]] = None,
top_n: int = 30,
) -> Dict[str, Any]:
dsc.ensure_dbband_backtest_tables(db)
base = dict(base_cfg or bbe.get_dbband_defaults_from_db(db))
snap = db.get_merged_env_snapshot() if hasattr(db, "get_merged_env_snapshot") else {}
raw_candles = hb.get_stored_min_candles(db, code, start_date, end_date, tf_min=int(tf))
candles = bbc.normalize_stored_min_candles(raw_candles)
if not candles:
return {"error": f"{code} {tf}분봉 없음 — holding_min_candles 수집 필요", "results": []}
fee_rate = float(snap.get("FEE_RATE_PCT") or 0.015) / 100
sell_tax = float(snap.get("SELL_TAX_RATE_PCT") or 0.18) / 100
slot = float(base.get("slot_money") or snap.get("DBBAND_SLOT_MONEY") or 3_000_000)
axes = _grid_axes(snap)
combos: List[Dict[str, Any]] = []
for inn, out, ma, sl, tm, rr in product(
axes["bb_inner_std"],
axes["bb_outer_std"],
axes["trend_ma_period"],
axes["stop_loss_pct"],
axes["tp_mode"],
axes["rr_ratio"],
):
p = dict(base)
p["bb_inner_std"] = inn
p["bb_outer_std"] = out
p["trend_ma_period"] = ma
p["sl_pct"] = float(sl) / 100.0
p["stop_loss_pct"] = float(sl)
p["tp_mode"] = tm
p["rr_ratio"] = rr
combos.append(p)
from kis_trader.backtest.backtest_portfolio_common import backtest_slip_pct
results: List[Dict[str, Any]] = []
for params in combos:
trades = bbe.run_dbband_backtest_single(candles, params)
bbc.attach_dbband_trade_pnl(
trades, slot_money=slot, fee_rate=fee_rate, sell_tax=sell_tax,
slip_pct=backtest_slip_pct(params),
)
total_pnl = sum(int(t.get("pnl") or 0) for t in trades)
wins = sum(1 for t in trades if int(t.get("pnl") or 0) > 0)
n = len(trades)
results.append({
"params": params,
"apply_cfg": params_to_dbband_env_patch(params),
"total_trades": n,
"win_rate": round(wins / n * 100, 1) if n else 0.0,
"total_pnl": total_pnl,
})
results.sort(key=lambda x: (x["total_pnl"], x["win_rate"]), reverse=True)
return {
"code": code,
"start": start_date,
"end": end_date,
"timeframe": int(tf),
"tested_combos": len(combos),
"candle_count": len(candles),
"results": results[:top_n],
}
def main() -> None:
today = datetime.now().date()
parser = argparse.ArgumentParser(description="DBBAND 종목별 파라미터 탐색")
parser.add_argument("--symbol", "--code", dest="code", required=True)
parser.add_argument("--start", default=(today - timedelta(days=30)).isoformat())
parser.add_argument("--end", default=today.isoformat())
parser.add_argument("--tf", type=int, default=15)
parser.add_argument("--top", type=int, default=50)
parser.add_argument("--save-top", action="store_true", help="1위를 dbband_stock_config에 저장")
args = parser.parse_args()
db = TradeDB()
try:
dsc.ensure_dbband_backtest_tables(db)
base = dsc.load_dbband_engine_cfg(db, args.code, bbe.get_dbband_defaults_from_db(db))
out = run_search_for_code(
db, args.code, args.start, args.end, args.tf, base_cfg=base, top_n=args.top,
)
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
sym = str(args.code).upper()
path = os.path.join(_results_dir(), f"search_dbband_{sym}_{ts}.json")
with open(path, "w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False, indent=2)
print(f"saved {path}")
if args.save_top and out.get("results"):
top = out["results"][0]["params"]
dsc.set_dbband_stock_config(db, sym, sym, top, tf_min=args.tf)
print(f"applied top-1 → dbband_stock_config ({sym})")
finally:
db.close()
if __name__ == "__main__":
main()