Files
kis_bot/remove/legacy_root/diag_momentum_backtest.py
Your Name 6d2a706a48 커밋 1 — 실매 가격 TTL 구멍 (본체)
왜: 체결이 없어도 마지막가는 유지인데, TTL로 None 만들고 매도/EOD를 건너뛰어 8/5 돌파·금요일 leftover가 남음. 호가필터 TTL 구멍과 같은 병.

넣을 파일

신규: kis_trader/engine/live_sell_price.py
kis_trader/strategies/base.py (_ws_last_quote, _resolve_sell_price)
전략: momentum.py scalping.py tail_catch.py breakout.py range_break.py dart_strategy.py updow_strategy.py updown_feed.py us_momentum.py
WS: ws_manager.py kis_ws.py kiwoom_ws.py ls_ws.py kis_ws_overseas.py
kis_trader/web/live_config_schema.py (WS_PRICE_MAX_AGE_SEC 기본 0)
database.py (키 주석 + legacy/ sys.path)
kis_trader/execution/order_manager.py (잔고 있는데 40240000 ghost_purge 금지 — 같은 EOD 사고)
EOD가 min_hold에 안 막히게 손본 momentum_hts_logic.py / scalping_engine.py / tail_engine.py (이 대화에서 손본 부분만 확인 후)
문서: docs/like_mcp.md/db_erd.md code_architecture.md (가격 TTL 문구)
메시지 초안

fix: 매수·매도 현재가를 TTL로 버리지 않음 (마지막 RAM)
횡보·체결 공백을 죽은 캐시로 오인해 None 처리하면 손절·EOD가 스킵된다.
호가필터와 같이 나이는 무시하고 마지막 체결가를 유지한다. EOD는 매수가 폴백.
영향: 실매 O / 백테·옵투나 봉 경로 거의 무관 (엔진 식 변경 아님)

커밋 2 — 루트 정리 (remove/ vs legacy/)
왜: 루트 단독봇·테스트는 지울 보관함으로. 웹·알람이 아직 쓰는 모듈은 remove에 두면 나중에 폴더째 삭제 때 깨짐.

넣을 파일

이동: 미사용 → remove/legacy_root/ (래퍼, ETF/키움 옛봇, 테스트, kiwoom_rest_api 등)
이동: 사용 중 → legacy/ (holding_bot kis_holding_ver1 news_analyzer kis_long_ver1/2)
신규: kis_trader/utils/legacy_root.py legacy/README.md remove/README.md
import 경로: backtest_web.py mm_butler.py mm_remote.py updow_holding_cfg.py dbband_stock_cfg.py param_search_updow*.py dbband_param_search.py param_search_apply_snapshot.py verify_three_paths.py
docs/like_mcp.md/code_architecture.md 수동 노트
메시지 초안

chore: 미사용 루트는 remove/, 웹·알람 구모듈은 legacy/
remove는 나중에 통째 삭제 예정. holding_bot·news_analyzer·kis_long은
ensure_legacy_root로 legacy/만 본다.
빼기: scratch/set_ws_price_max_age_zero.py (일회성)
2026-08-18 00:13:17 +09:00

174 lines
7.6 KiB
Python

#!/usr/bin/env python3
"""
모멘텀 1위 파라미터 CLI 진단 백테스트 (웹 타임아웃 우회용 · 보존 스크립트)
목적
- 웹 모멘텀 백테가 5일치에서 타임아웃 → 같은 엔진(run_momentum_backtest_web_aligned)을
CLI 로 직접 돌려, 일자별 손익 + 필터 OFF/ON 비교를 한 화면에서 본다.
- "웹 입력값(UI) ↔ 엔진 파라미터" 매핑을 나란히 출력해, trail_pct 같은 값이
중간에 깨지지 않는지 사람이 눈으로 검증한다.
사용
.venv/bin/python diag_momentum_backtest.py # 최신 JSON 1위, 5일
.venv/bin/python diag_momentum_backtest.py --rank 1 --start 2026-06-22 --end 2026-06-26
.venv/bin/python diag_momentum_backtest.py --json <경로>
주의
- 웹과 동일하게 universe=history(MOMENTUM 저장 이력) 사용.
- 틱재생은 코드 기본값(OFF) 그대로 → 웹·파람서치 동일 1분봉 OHLC 경로.
"""
import sys, os, json, glob, argparse, logging
from datetime import datetime
logging.getLogger("TradeDB").setLevel(logging.WARNING)
from database import TradeDB
from kis_trader.backtest import momentum_backtest_common as mbc
def _latest_json() -> str:
pats = [
os.path.join("kis_trader", "backtest", "results", "search_momentum_*.json"),
os.path.join(os.path.expanduser("~"), ".kis_bot_search_results", "search_momentum_*.json"),
]
found = []
for p in pats:
found.extend(glob.glob(p))
if not found:
sys.exit("❌ search_momentum_*.json 을 찾을 수 없습니다. --json 으로 직접 지정하세요.")
return max(found, key=os.path.getmtime)
def main():
ap = argparse.ArgumentParser(description="모멘텀 1위 CLI 진단 백테스트")
ap.add_argument("--json", default=None, help="param_search 결과 JSON (미지정 시 최신)")
ap.add_argument("--rank", type=int, default=1, help="JSON 내 순위 (기본 1위)")
ap.add_argument("--start", default=None, help="시작일 YYYY-MM-DD (미지정 시 JSON 기간)")
ap.add_argument("--end", default=None, help="종료일 YYYY-MM-DD")
args = ap.parse_args()
json_path = args.json or _latest_json()
with open(json_path, encoding="utf-8") as f:
data = json.load(f)
start = args.start or data.get("start")
end = args.end or data.get("end")
top = data.get("top") or []
if args.rank < 1 or args.rank > len(top):
sys.exit(f"❌ rank {args.rank} 없음 (JSON 보유 {len(top)}개)")
entry = top[args.rank - 1]
ui = entry.get("params") or {} # 웹 폼에 입력될 값(그리드 축)
merged = dict(entry.get("merged_params") or {}) # 엔진에 그대로 들어간 값
slot = float(merged.get("slot_money") or data.get("slot_money") or 300000.0)
max_stocks = int(merged.get("max_stocks") or data.get("max_stocks") or 20)
budget = float(merged.get("total_budget_krw") or data.get("total_budget_krw") or 6000000.0)
fee_rate = float(merged.get("fee_rate") or 0.00015)
sell_tax = float(merged.get("sell_tax") or 0.0018)
# merged 의 fee/tax 가 퍼센트(0.015)로 저장된 옛 JSON 방어 → 비율로 정규화
if fee_rate > 0.01:
fee_rate /= 100.0
if sell_tax > 0.05:
sell_tax /= 100.0
print("=" * 78)
print(f"📂 JSON : {json_path}")
print(f"🏅 순위 : {args.rank}위 | 기간: {start} ~ {end}")
print(f"💰 JSON기준: 손익 {entry.get('total_pnl', 0):+,.0f}원 | "
f"승률 {entry.get('win_rate', 0):.1f}% | 거래 {entry.get('total_trades', 0)} | "
f"PF {entry.get('pf', 0):.2f} (파람서치 필터 OFF)")
print("=" * 78)
# ── 웹 입력(UI) ↔ 엔진 파라미터 비교 (값 깨짐 검증) ──
print("\n[웹 입력값 ↔ 엔진 파라미터 매핑]")
cmp_keys = [
("trail_pct", "trail_pct"), ("trail_arm_pct", "trail_arm_pct"),
("sl_pct", "sl_pct"), ("tp_pct", "tp_pct"), ("tp_max_pct", "tp_max_pct"),
("shoulder_min_high", "shoulder_min_high"), ("shoulder_cut_pct", "shoulder_cut_pct"),
("mom_rsi_min", "mom_rsi_min"), ("mom_vol_mult", "mom_vol_mult"),
("pattern_breakout", "pattern_breakout"), ("pattern_pullback", "pattern_pullback"),
("use_ema_filter", "use_ema_filter"),
("max_spread_pct", "max_spread_pct"),
]
print(f" {'필드':22s} {'UI(폼)':>12s} {'엔진(merged)':>12s}")
for uk, mk in cmp_keys:
uv = ui.get(uk, "-")
mv = merged.get(mk, "-")
print(f" {uk:22s} {str(uv):>12s} {str(mv):>12s}")
# ── 데이터 로딩 (param_search 와 동일 소스) ──
print("\n⏳ 캔들 로딩...")
from kis_trader.backtest.param_search_momentum import _load_candles_for_search
rsi_period = int(merged.get("rsi_period", 3))
codes_candles = _load_candles_for_search(start, end, rsi_period)
print(f" 종목 {len(codes_candles):,}")
print("⏳ 유니버스(MOMENTUM 저장 이력)...")
hist, src, n_bins, _iv, timing = mbc.resolve_momentum_universe(
start.replace("-", ""), end.replace("-", ""),
use_saved_history=True, strategy_id="MOMENTUM",
)
print(f" src={src} bins={n_bins} timing={timing}")
period_days = max(1, (datetime.strptime(end, "%Y-%m-%d")
- datetime.strptime(start, "%Y-%m-%d")).days + 1)
start_key = start.replace("-", "") + "0000"
end_key = end.replace("-", "") + "2359"
# merged_params 는 UI 스케일(sl_pct=1.5 = 1.5%). 엔진은 비율(0.015)을 기대하므로
# param-search·웹과 동일하게 _ui_to_engine_params 로 변환한 뒤 엔진에 넣는다.
from kis_trader.backtest.param_search_momentum import _ui_to_engine_params
def run(label, ob_on, pg_on):
ep = _ui_to_engine_params(dict(merged))
ep["slot_money"] = slot
ep["max_stocks"] = max_stocks
ep["total_budget_krw"] = budget
ep["portfolio_mode"] = True
ep["scan_interval_min"] = 1
ep["_orderbook_filter_enabled"] = ob_on
ep["_program_filter_enabled"] = pg_on
if ob_on or pg_on:
ep["backtest_use_kiwoom_body_snapshot"] = True
ep["_backtest_use_kiwoom_body"] = True
db = TradeDB()
meta = {"db": db, "start_key": start_key, "end_key": end_key}
try:
trades = mbc.run_momentum_backtest_web_aligned(
codes_candles, ep, hist,
slot_money=slot, fee_rate=fee_rate, sell_tax=sell_tax,
max_stocks=max_stocks, total_budget_krw=budget,
meta_out=meta,
)
finally:
db.close()
st = mbc.summarize_momentum_trades(
trades, total_budget_krw=budget, period_days=period_days,
)
print(f"\n── {label} ──")
print(f" 총손익 {st['total_pnl']:+,.0f}원 | 승률 {st['win_rate']:.1f}% | "
f"거래 {st['total_trades']} | PF {st.get('pf', 0):.2f}")
daily = {}
for t in trades:
day = str(t.get("buy_time") or t.get("entry_time") or "")[:8]
d = daily.setdefault(day, [0, 0.0])
d[0] += 1
d[1] += float(t.get("pnl") or 0)
for day in sorted(daily):
n, p = daily[day]
print(f" {day}: {n:3d}거래 {p:+,.0f}")
return st
print("\n" + "=" * 78)
print("백테 실행 (웹과 동일 엔진 · 1분봉 OHLC)")
print("=" * 78)
run("① 필터 OFF (파람서치 1위 재현)", False, False)
run("② 호가필터 ON (웹 체크박스 호가만)", True, False)
run("③ 호가+프로그램 ON (웹 기본 체크 그대로)", True, True)
print("\n✅ 완료")
if __name__ == "__main__":
main()