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:
122
kis_trader/backtest/tail_exit_ab_test.py
Normal file
122
kis_trader/backtest/tail_exit_ab_test.py
Normal file
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
"""꼬리 청산 A/B — baseline vs 래칫 vs 시간컷 (fast 5위 base, 2026-05)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from database import TradeDB
|
||||
from kis_trader.backtest import tail_backtest_common as tbc
|
||||
from kis_trader.engine import tail_engine as te
|
||||
|
||||
START = "2026-05-01"
|
||||
END = "2026-05-31"
|
||||
TF = 3
|
||||
|
||||
SCENARIOS = [
|
||||
("baseline_DB(래칫ON·어깨0.5%·트레일OFF)", {}),
|
||||
("어깨2%+trail1.5/1.5_래칫ON", {
|
||||
"shoulder_min_high": 0.02,
|
||||
"trail_pct": 0.015,
|
||||
"trail_arm_pct": 0.015,
|
||||
}),
|
||||
("어깨2%+trail1.5/1.5_래칫OFF", {
|
||||
"ratchet_tiers": "",
|
||||
"shoulder_min_high": 0.02,
|
||||
"trail_pct": 0.015,
|
||||
"trail_arm_pct": 0.015,
|
||||
}),
|
||||
("어깨2%만_트레일OFF_래칫ON", {
|
||||
"shoulder_min_high": 0.02,
|
||||
"trail_pct": 0.0,
|
||||
"trail_arm_pct": 0.0,
|
||||
}),
|
||||
("어깨2%만_트레일OFF_래칫OFF", {
|
||||
"ratchet_tiers": "",
|
||||
"shoulder_min_high": 0.02,
|
||||
"trail_pct": 0.0,
|
||||
"trail_arm_pct": 0.0,
|
||||
}),
|
||||
]
|
||||
|
||||
|
||||
def _load_base_params() -> dict:
|
||||
"""DB 최신값 그대로 — fast 파라서치·실매와 동일."""
|
||||
d = te.get_tail_defaults_from_db()
|
||||
d["portfolio_mode"] = True
|
||||
d["force_eod_exit"] = False
|
||||
d.setdefault("max_hold_bars", 0)
|
||||
return d
|
||||
|
||||
|
||||
def main() -> None:
|
||||
db = TradeDB()
|
||||
try:
|
||||
start_key, end_key, start_ymd, end_ymd = tbc.date_keys(START, END)
|
||||
candles_by_code, _, _ = tbc.load_tail_candles_by_code(
|
||||
db, start_key, end_key, TF, rsi_period=14,
|
||||
)
|
||||
universe_by_slot, _, _, _ = tbc.resolve_tail_universe(
|
||||
start_ymd, end_ymd, use_saved_history=True,
|
||||
)
|
||||
snap = db.get_merged_env_snapshot()
|
||||
fee_rate, sell_tax, _ = tbc.fee_and_slot_from_env_row(snap)
|
||||
base = _load_base_params()
|
||||
period_days = max(1, (datetime.strptime(END, "%Y-%m-%d") - datetime.strptime(START, "%Y-%m-%d")).days + 1)
|
||||
slot = float(base.get("slot_money") or 200000)
|
||||
mxs = int(base.get("max_stocks") or 20)
|
||||
tb = float(base.get("total_budget_krw") or mxs * slot)
|
||||
|
||||
rows = []
|
||||
for label, patch in SCENARIOS:
|
||||
params = dict(base)
|
||||
params.update(patch)
|
||||
meta: dict = {}
|
||||
trades = tbc.run_tail_backtest_web_aligned(
|
||||
candles_by_code, params, universe_by_slot,
|
||||
slot_money=slot, fee_rate=fee_rate, sell_tax=sell_tax,
|
||||
max_stocks=mxs, total_budget_krw=tb, meta_out=meta,
|
||||
)
|
||||
stats = tbc.summarize_tail_trades(
|
||||
trades, total_budget_krw=tb, period_days=period_days,
|
||||
)
|
||||
reasons: dict = {}
|
||||
for t in trades:
|
||||
k = str(t.get("sell_reason") or t.get("reason") or "?")
|
||||
reasons[k] = reasons.get(k, 0) + 1
|
||||
rows.append({
|
||||
"scenario": label,
|
||||
"total_trades": stats["total_trades"],
|
||||
"win_rate": stats["win_rate"],
|
||||
"total_pnl": stats["total_pnl"],
|
||||
"pf": stats["pf"],
|
||||
"avg_hold_min": stats["avg_hold_min"],
|
||||
"sell_reasons": reasons,
|
||||
"patch": patch,
|
||||
})
|
||||
print(
|
||||
f"{label:32} | 거래 {stats['total_trades']:4} | "
|
||||
f"승률 {stats['win_rate']:5.1f}% | PnL {stats['total_pnl']:>9,} | "
|
||||
f"PF {stats['pf']:.2f} | 보유 {stats['avg_hold_min']:.0f}분 | {reasons}"
|
||||
)
|
||||
|
||||
out = {
|
||||
"start": START, "end": END, "timeframe": TF,
|
||||
"base": {k: base.get(k) for k in (
|
||||
"min_drop_rate", "shoulder_min_high", "shoulder_cut_pct",
|
||||
"ratchet_tiers", "trail_pct", "trail_arm_pct",
|
||||
"stop_atr_mult", "target_atr_mult", "max_daily_change",
|
||||
"entry_mode", "slot_money", "max_stocks", "total_budget_krw",
|
||||
)},
|
||||
"scenarios": rows,
|
||||
}
|
||||
path = f"/home/hoon/kis_bot/kis_trader/backtest/results/tail_exit_ab_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(out, f, ensure_ascii=False, indent=2)
|
||||
print(f"\n💾 저장: {path}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user