커밋 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 (일회성)
This commit is contained in:
152
remove/legacy_root/_verify_shared_e2e.py
Normal file
152
remove/legacy_root/_verify_shared_e2e.py
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
공유메모리 틱 배선 end-to-end 검증 — 동일 데이터로 dict vs SharedTicksMapping 백테 2회 대조.
|
||||
|
||||
거래가 확인된 실 파라미터(merged_params, 246거래)로 주간 백테를 두 번 돌린다:
|
||||
A) ticks_by_code = 원본 dict (기존 경로)
|
||||
B) ticks_by_code = SharedTicksMapping (공유메모리 컬럼 경로)
|
||||
두 결과의 trades 리스트와 요약통계가 완전히 동일해야 한다(bit-identical).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from database import TradeDB
|
||||
from kis_trader.backtest import momentum_backtest_common as mbc
|
||||
from kis_trader.backtest.param_search_momentum import _load_candles_for_search
|
||||
from kis_trader.backtest.momentum_backtest_common import resolve_momentum_universe
|
||||
from kis_trader.backtest.momentum_tick_loader import load_momentum_ticks_by_code
|
||||
from kis_trader.backtest.shared_ticks import build_shared_ticks
|
||||
|
||||
START = "2026-06-28"
|
||||
END = "2026-07-05"
|
||||
JSON = "./kis_trader/backtest/results/search_momentum_fast_20260705_030415.json"
|
||||
|
||||
|
||||
def run_backtest(codes_candles, universe, ticks_by_code, engine_params, slot_money,
|
||||
max_stocks, total_budget, start_key, end_key):
|
||||
p = dict(engine_params)
|
||||
p["slot_money"] = float(slot_money)
|
||||
p["max_stocks"] = int(max_stocks)
|
||||
p["total_budget_krw"] = float(total_budget)
|
||||
p["portfolio_mode"] = True
|
||||
p["_backtest_period_start_key"] = start_key[:12]
|
||||
meta = {"start_key": start_key, "end_key": end_key} # db 없음 → 틱 재로딩 안 함
|
||||
trades = mbc.run_momentum_backtest_web_aligned(
|
||||
codes_candles, p, universe,
|
||||
slot_money=slot_money, fee_rate=0.00015, sell_tax=0.0018,
|
||||
max_stocks=max_stocks, total_budget_krw=total_budget,
|
||||
ticks_by_code=ticks_by_code,
|
||||
orderbook_by_code={}, program_by_code={},
|
||||
meta_out=meta,
|
||||
)
|
||||
stats = mbc.summarize_momentum_trades(trades, total_budget_krw=total_budget, period_days=1)
|
||||
return trades, stats
|
||||
|
||||
|
||||
def norm_trades(trades):
|
||||
"""비교용 정규화 — 딕셔너리 리스트를 정렬 가능한 튜플 리스트로."""
|
||||
out = []
|
||||
for t in trades:
|
||||
out.append(tuple(sorted(
|
||||
(k, (round(v, 6) if isinstance(v, float) else v))
|
||||
for k, v in t.items()
|
||||
)))
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
meta_j = json.load(open(JSON))
|
||||
r0 = meta_j["top"][0]
|
||||
engine_params = dict(r0["merged_params"])
|
||||
slot_money = float(meta_j.get("slot_money") or 3_000_000)
|
||||
max_stocks = int(meta_j.get("max_stocks") or 3)
|
||||
total_budget = float(meta_j.get("total_budget_krw") or 9_000_000)
|
||||
rsi_period = int(engine_params.get("rsi_period", 3))
|
||||
|
||||
start_key = START.replace("-", "") + "0000"
|
||||
end_key = END.replace("-", "") + "2359"
|
||||
|
||||
print("⏳ 캔들 로드...")
|
||||
codes_candles = _load_candles_for_search(START, END, rsi_period)
|
||||
print(f" 종목 {len(codes_candles):,}")
|
||||
|
||||
print("⏳ 유니버스 로드...")
|
||||
history, src, n_bins, _iv, timing = resolve_momentum_universe(
|
||||
START.replace("-", ""), END.replace("-", ""),
|
||||
use_saved_history=True, strategy_id="MOMENTUM",
|
||||
)
|
||||
universe = history
|
||||
print(f" 유니버스 슬롯 {len(universe or {}):,} (src={src})")
|
||||
|
||||
print("⏳ ws_ticks 로드...")
|
||||
db = TradeDB()
|
||||
try:
|
||||
ticks_dict, tick_rows = load_momentum_ticks_by_code(
|
||||
db, start_key, end_key, set(codes_candles.keys()),
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
print(f" 틱 {tick_rows:,}건 / 종목 {len(ticks_dict):,}")
|
||||
|
||||
# 거래를 더 많이 발생시켜 매도/진입/휩쏘 경로를 넓게 커버하는 변형들.
|
||||
variants = [
|
||||
("baseline(top)", {}),
|
||||
("loose_entry", {"mom_rsi_min": 0, "mom_vol_mult": 1.0, "pattern_breakout": 1,
|
||||
"pattern_pullback": 1, "use_ema_filter": 0}),
|
||||
("tight_sl", {"mom_rsi_min": 0, "mom_vol_mult": 1.0, "use_ema_filter": 0,
|
||||
"sl_pct": 0.7, "tp_pct": 2.0, "trail_pct": 0.5, "trail_arm_pct": 0.4}),
|
||||
("wide_tp", {"mom_rsi_min": 0, "mom_vol_mult": 1.0, "use_ema_filter": 0,
|
||||
"sl_pct": 3.0, "tp_pct": 12.0, "tp_max_pct": 20.0, "shoulder_cut_pct": 0.5}),
|
||||
]
|
||||
|
||||
print("▶ 공유메모리 store 빌드...")
|
||||
store = build_shared_ticks(ticks_dict)
|
||||
if store is None:
|
||||
print("❌ build_shared_ticks None — 검증 불가")
|
||||
return 1
|
||||
|
||||
all_ok = True
|
||||
total_trades_checked = 0
|
||||
try:
|
||||
for name, override in variants:
|
||||
ep = dict(engine_params)
|
||||
ep.update(override)
|
||||
trades_a, stats_a = run_backtest(codes_candles, universe, ticks_dict, ep,
|
||||
slot_money, max_stocks, total_budget, start_key, end_key)
|
||||
shared_map = store.attach_mapping()
|
||||
trades_b, stats_b = run_backtest(codes_candles, universe, shared_map, ep,
|
||||
slot_money, max_stocks, total_budget, start_key, end_key)
|
||||
na, nb = norm_trades(trades_a), norm_trades(trades_b)
|
||||
ok = True
|
||||
if len(na) != len(nb):
|
||||
ok = False
|
||||
detail = f"거래수 {len(na)} vs {len(nb)}"
|
||||
else:
|
||||
diff = [i for i in range(len(na)) if na[i] != nb[i]]
|
||||
if diff:
|
||||
ok = False
|
||||
detail = f"내용불일치 {len(diff)}건 (idx {diff[0]})"
|
||||
print(" dict :", dict(na[diff[0]]))
|
||||
print(" shared:", dict(nb[diff[0]]))
|
||||
else:
|
||||
detail = "동일"
|
||||
for k in ("total_trades", "total_pnl", "win_rate", "pf", "mdd"):
|
||||
if stats_a.get(k) != stats_b.get(k):
|
||||
ok = False
|
||||
detail += f" | {k} {stats_a.get(k)}≠{stats_b.get(k)}"
|
||||
total_trades_checked += len(na)
|
||||
all_ok = all_ok and ok
|
||||
print(f" {'✅' if ok else '❌'} {name:16s} 거래 {len(na):3d} · 손익 {stats_a['total_pnl']:+,.0f} · {detail}")
|
||||
finally:
|
||||
store.unlink()
|
||||
|
||||
print(f"\n대조한 총 거래 {total_trades_checked}건 (변형 {len(variants)}개)")
|
||||
print("🎉 bit-identical 통과" if all_ok else "🚨 불일치 발생")
|
||||
return 0 if all_ok else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user