변경 사항 ---- - _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>
101 lines
3.4 KiB
Python
101 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""모멘텀 청산 A/B — 어깨·래칫·트레일·시간컷 (실매 check_sell_signal_momentum_live 동일 우선순위)."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime
|
|
|
|
from database import TradeDB
|
|
from kis_trader.backtest import momentum_backtest_common as mbc
|
|
from kis_trader.engine import momentum_engine as me
|
|
|
|
START = "2026-05-01"
|
|
END = "2026-05-31"
|
|
|
|
SCENARIOS = [
|
|
("baseline_DB(전용청산)", {}),
|
|
("래칫_2:1.5,5:1,8:0.7", {
|
|
"ratchet_tiers": "2:1.5,5:1,8:0.7",
|
|
}),
|
|
("트레일1.5%_무장1%", {
|
|
"trail_pct": 0.015,
|
|
"trail_arm_pct": 0.01,
|
|
}),
|
|
("시간컷30분", {
|
|
"max_hold_bars": 30,
|
|
}),
|
|
("어깨0.5%_컷0.3%_래칫OFF", {
|
|
"ratchet_tiers": "",
|
|
"shoulder_min_high": 0.005,
|
|
"shoulder_cut_pct": 0.003,
|
|
}),
|
|
]
|
|
|
|
|
|
def main() -> None:
|
|
db = TradeDB()
|
|
try:
|
|
start_key, end_key, start_ymd, end_ymd = mbc.date_keys(START, END)
|
|
candles_by_code, _ = mbc.load_momentum_candles_by_code(db, start_key, end_key)
|
|
universe_by_slot, _, _, _, _ = mbc.resolve_momentum_universe(
|
|
start_ymd, end_ymd, use_saved_history=True,
|
|
)
|
|
snap = db.get_merged_env_snapshot()
|
|
fee_rate, sell_tax, slot = mbc.fee_and_slot_from_env_row(snap)
|
|
base = me.get_momentum_defaults_from_db(db)
|
|
base["portfolio_mode"] = True
|
|
base["force_eod_exit"] = False
|
|
portfolio = mbc.resolve_momentum_portfolio_params(snap, base, slot_money=slot)
|
|
slot_money = float(portfolio["slot_money"])
|
|
max_stocks = int(portfolio["max_stocks"])
|
|
total_budget = float(portfolio["total_budget_krw"])
|
|
period_days = max(
|
|
1,
|
|
(datetime.strptime(END, "%Y-%m-%d") - datetime.strptime(START, "%Y-%m-%d")).days + 1,
|
|
)
|
|
|
|
rows = []
|
|
for label, patch in SCENARIOS:
|
|
params = dict(base)
|
|
params.update(patch)
|
|
meta: dict = {}
|
|
trades = mbc.run_momentum_backtest_web_aligned(
|
|
candles_by_code, params, universe_by_slot,
|
|
slot_money=slot_money, fee_rate=fee_rate, sell_tax=sell_tax,
|
|
max_stocks=max_stocks, total_budget_krw=total_budget, meta_out=meta,
|
|
)
|
|
stats = mbc.summarize_momentum_trades(
|
|
trades, total_budget_krw=total_budget, period_days=period_days,
|
|
)
|
|
reasons = mbc.count_momentum_sell_reasons(trades)
|
|
rows.append({
|
|
"label": label,
|
|
"pnl": stats["total_pnl"],
|
|
"win_rate": stats["win_rate"],
|
|
"trades": stats["total_trades"],
|
|
"pf": stats["pf"],
|
|
"sell_reasons": reasons,
|
|
})
|
|
print(
|
|
f"{label:28s} | {stats['total_pnl']:>+10,.0f} | "
|
|
f"승률 {stats['win_rate']:5.1f}% | {stats['total_trades']:3}건 | "
|
|
f"PF {stats['pf']:.2f} | {reasons}"
|
|
)
|
|
|
|
out = {
|
|
"start": START,
|
|
"end": END,
|
|
"scenarios": rows,
|
|
}
|
|
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
path = f"kis_trader/backtest/results/momentum_exit_ab_{ts}.json"
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
json.dump(out, f, indent=2, ensure_ascii=False)
|
|
print(f"\n💾 저장: {path}")
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|