cd /home/hoon/kis_bot && python3 - <<'PY'
from kis_trader.utils.env import invalidate_merged_env_cache
from kis_trader.utils import env as em
invalidate_merged_env_cache()
m=em.get_merged_env_dict()
for k in sorted(m):
    if k.startswith('MOMENTUM_') and any(x in k for x in ('EMA','CHASE','PULL','ORDERBOOK','WHIPSAW','VOL_MULT','STOP','SETUP')):
        print(f'{k}={m[k]!r}')

# what get_momentum_defaults_from_db returns
from kis_trader.engine import momentum_engine as me
# find function
fn=getattr(me,'get_momentum_defaults_from_db',None)
if not fn:
    import kis_trader.backtest.param_search_momentum as p
    fn=p.get_momentum_defaults_from_db
d=fn()
print('\ndefaults ema', d.get('use_ema_filter'), 'chase', d.get('chase_lookback_min'), 'pull', d.get('pullback_lookback_min'), 'slow', d.get('ema_slow_period'))
print('ob related', {k:d[k] for k in d if 'order' in k.lower() or 'whip' in k.lower() or 'spread' in k.lower()})
PY
# check html defaults for mom fields
rg -n "id=\"mom_chase|id=\"mom_ema|id=\"mom_pull|id=\"mom_sl|id=\"mom_vol|id=\"mom_use_ema" /home/hoon/kis_bot/templates/backtest.html | head -30

DB defaults vs HTML defaults; whip in engine


Shell

cd /home/hoon/kis_bot && python3 - <<'PY'
from kis_trader.backtest.param_search_momentum import get_momentum_defaults_from_db
from kis_trader.utils.env import invalidate_merged_env_cache
invalidate_merged_env_cache()
d=get_momentum_defaults_from_db()
print('keys', len(d))
for k in ['use_ema_filter','chase_lookback_min','pullback_lookback_min','ema_slow_period','mom_vol_mult','sl_pct','setup_vol_max_mult']:
    print(k, d.get(k))
# bool of '0' and False
print('bool of use_ema raw from env')
from kis_trader.utils.env import get_env_bool
print('get_env_bool USE_EMA', get_env_bool('MOMENTUM_USE_EMA_FILTER', True))
# how does get_momentum read use_ema?
import inspect
src=inspect.getsource(get_momentum_defaults_from_db)
# print relevant lines
for line in src.splitlines():
    if 'ema' in line.lower() or 'chase' in line.lower() or 'pullback' in line.lower():
        print(line)
PY

Inspect get_momentum_defaults_from_db EMA/chase


cd /home/hoon/kis_bot && python3 - <<'PY'
from kis_trader.utils.env import invalidate_merged_env_cache
invalidate_merged_env_cache()
from kis_trader.engine.momentum_engine import get_momentum_defaults_from_db
d=get_momentum_defaults_from_db()
for k in ['use_ema_filter','chase_lookback_min','pullback_lookback_min','ema_slow_period','mom_vol_mult','sl_pct','setup_vol_max_mult','pullback_min_pct']:
    print(k, '=', d.get(k))
PY
# why fill might fail - check catch on env params
rg -n "catch|\.catch|fillMomentum|/api/env/params" /home/hoon/kis_bot/static/js/backtest.js | head -40
Read


cd /home/hoon/kis_bot && python3 - <<'PY'
from kis_trader.utils.env import invalidate_merged_env_cache
invalidate_merged_env_cache()
from kis_trader.engine.momentum_engine import get_momentum_defaults_from_db
d=get_momentum_defaults_from_db()
for k in ['use_ema_filter','chase_lookback_min','pullback_lookback_min','ema_slow_period','setup_vol_max_mult','pattern_breakout']:
    print(repr(k), 'in', k in d, 'val', repr(d.get(k)))
# find where these should be set in return dict
import inspect
src=inspect.getsource(get_momentum_defaults_from_db)
print('has chase in source', 'chase_lookback' in src)
print('has use_ema in source', 'use_ema_filter' in src)
# show return keys containing ema/chase
print([k for k in d if 'ema' in k.lower() or 'chase' in k.lower() or 'pull' in k.lower() or 'setup' in k.lower()])
PY

Check if V4 keys missing from engine defaults


근본원인: `get_momentum_defaults_from_db()`에 V4(추격/EMA/눌림) 키가 빠져 백테가 코드 기본값으로 돕니다. 폼 로드와 엔진 기본값을 DB와 맞추겠습니다.


StrReplace

        use_high_chase_f = momentum_env_bool(r, "MOMENTUM_USE_HIGH_CHASE_FILTER", False)
        use_daily_range_f = momentum_env_bool(r, "MOMENTUM_USE_DAILY_RANGE_FILTER", False)
        use_rsi_filter = momentum_env_bool(r, "MOMENTUM_USE_RSI_FILTER", False)
        hts_flags = hts_trigger_defaults_from_row(r)

        use_high_chase_f = momentum_env_bool(r, "MOMENTUM_USE_HIGH_CHASE_FILTER", False)
        use_daily_range_f = momentum_env_bool(r, "MOMENTUM_USE_DAILY_RANGE_FILTER", False)
        use_rsi_filter = momentum_env_bool(r, "MOMENTUM_USE_RSI_FILTER", False)
        # V4 TRIGGER — 웹백테/CLI가 이 dict 만 보면 DB와 동일 (누락 시 HTML·코드 기본값으로 어긋남)
        use_ema_filter = momentum_env_bool(r, "MOMENTUM_USE_EMA_FILTER", False)
        use_rsi_max_filter = momentum_env_bool(r, "MOMENTUM_USE_RSI_MAX_FILTER", False)
        ema_fast_period = momentum_env_int(r, "MOMENTUM_EMA_FAST_PERIOD", 9)
        ema_slow_period = momentum_env_int(r, "MOMENTUM_EMA_SLOW_PERIOD", 21)
        pattern_breakout = momentum_env_bool(r, "MOMENTUM_PATTERN_BREAKOUT", True)
        pattern_pullback = momentum_env_bool(r, "MOMENTUM_PATTERN_PULLBACK", True)
        chase_lookback_min = momentum_env_int(r, "MOMENTUM_CHASE_LOOKBACK_MIN", 10)
        pullback_lookback_min = momentum_env_int(r, "MOMENTUM_PULLBACK_LOOKBACK_MIN", 15)
        pullback_min_pct = momentum_env_float(r, "MOMENTUM_PULLBACK_MIN_PCT", 0.3)
        pullback_max_pct = momentum_env_float(r, "MOMENTUM_PULLBACK_MAX_PCT", 3.0)
        setup_vol_max_mult = momentum_env_float(r, "MOMENTUM_SETUP_VOL_MAX_MULT", 0.8)
        setup_bear_bars_min = momentum_env_int(r, "MOMENTUM_SETUP_BEAR_BARS_MIN", 1)
        hts_flags = hts_trigger_defaults_from_row(r)


        use_high_chase_f, use_daily_range_f = False, False
        use_rsi_filter = False
        use_ema_filter, use_rsi_max_filter = False, False
        ema_fast_period, ema_slow_period = 9, 21
        pattern_breakout, pattern_pullback = True, True
        chase_lookback_min, pullback_lookback_min = 10, 15
        pullback_min_pct, pullback_max_pct = 0.3, 3.0
        setup_vol_max_mult, setup_bear_bars_min = 0.8, 1
        hts_flags = hts_trigger_defaults_from_row({})
        slot_money, max_stocks, total_budget = 3_000_000, 3, 0
        min_hold_sec, live_align, lookback_bars = 30.0, True, 1
        use_forming, cd_floor, cd_engine_only = True, True, True
        skip_pre_sub = False
        force_eod, portfolio_mode = False, True
        eod_enabled, eod_hm = True, "15:20"
        exit_ob_enabled, exit_ob_ratio_min, exit_ob_ma_window = False, 0.4, 5
        exit_ob_min_profit_pct, exit_ob_min_hold_bars = 0.005, 3
        stop_ob_enabled, stop_ob_ratio_min, stop_ob_ma_window = False, 0.4, 5
        stop_ob_min_loss_pct, stop_ob_min_hold_bars = 0.003, 2
    finally:
        if own_db is not None:
            try:
                own_db.close()
            except Exception:
                pass

    return {
        "rsi_period": rsi_period,
        "mom_rsi_min": mom_rsi_min,
        "mom_rsi_max": mom_rsi_max,
        "mom_vol_mult": mom_vol_mult,
        "mom_vol_win": mom_vol_win,
        "mom_time_end_hm": mom_time_end,
        "e_min_chg_pct": e_min_chg_pct,
        "mom_max_from_open_pct": mom_max_open,
        "mom_min_from_open_pct": mom_min_open,
        "sl_pct": sl_pct,
        "tp_pct": tp_pct,
        "tp_max_pct": tp_max,
        "shoulder_min_high": shoulder_high,
        "shoulder_cut_pct": shoulder_cut,
        "ratchet_tiers": ratchet_tiers,
        "trail_pct": trail_pct,
        "trail_arm_pct": trail_arm_pct,
        "max_hold_bars": max_hold_bars,
        "max_daily": max_daily,
        "min_price": min_price,
        "max_daily_chg": max_daily_chg,
        "high_chase_thr": high_chase,
        "max_loss_krw": float(max_loss_krw),
        "min_drop_pct_for_loss_cut": min_drop_pct_for_loss_cut,
        "cooldown_min": cooldown_sec / 60.0,
        "time_start_hm": time_start,
        "time_end_hm": time_end,
        "skip_hts_scan_dupes": skip_hts,
        "use_defense_filters": use_defense,
        "use_high_chase_filter": use_high_chase_f,
        "use_daily_range_filter": use_daily_range_f,
        "use_rsi_filter": use_rsi_filter,
        "use_ema_filter": use_ema_filter,
        "use_rsi_max_filter": use_rsi_max_filter,
        "ema_fast_period": ema_fast_period,
        "ema_slow_period": ema_slow_period,
        "pattern_breakout": pattern_breakout,
        "pattern_pullback": pattern_pullback,
        "chase_lookback_min": chase_lookback_min,
        "pullback_lookback_min": pullback_lookback_min,
        "pullback_min_pct": pullback_min_pct,
        "pullback_max_pct": pullback_max_pct,
        "setup_vol_max_mult": setup_vol_max_mult,
        "setup_bear_bars_min": setup_bear_bars_min,
        **hts_flags,
        "slot_money": float(slot_money),
        "max_stocks": max_stocks,
        "total_budget_krw": float(total_budget),
        "min_hold_sec": min_hold_sec,
        "live_backtest_align": live_align,
        "live_signal_lookback_bars": lookback_bars,
        "live_align_use_forming_bar": use_forming,
        "cooldown_use_candle_floor": cd_floor,
        "cooldown_engine_only": cd_engine_only,
        "backtest_skip_pre_subscribe": skip_pre_sub,
        "eod_enabled": eod_enabled,
        "eod_hm": eod_hm,
        "force_eod_exit": eod_enabled,
        "portfolio_mode": portfolio_mode,
        "scan_interval_min": 1,
        # 수익구간 호가매도
        "exit_ob_enabled": exit_ob_enabled,
        "exit_ob_ratio_min": exit_ob_ratio_min,
        "exit_ob_ma_window": exit_ob_ma_window,
        "exit_ob_min_profit_pct": exit_ob_min_profit_pct,
        "exit_ob_min_hold_bars": exit_ob_min_hold_bars,
        # 손절호가
        "stop_ob_enabled": stop_ob_enabled,
        "stop_ob_ratio_min": stop_ob_ratio_min,
        "stop_ob_ma_window": stop_ob_ma_window,
        "stop_ob_min_loss_pct": stop_ob_min_loss_pct,
        "stop_ob_min_hold_bars": stop_ob_min_hold_bars,
    }

        use_high_chase_f, use_daily_range_f = False, False
        use_rsi_filter = False
        hts_flags = hts_trigger_defaults_from_row({})
        slot_money, max_stocks, total_budget = 3_000_000, 3, 0
        min_hold_sec, live_align, lookback_bars = 30.0, True, 1
        use_forming, cd_floor, cd_engine_only = True, True, True
        skip_pre_sub = False
        force_eod, portfolio_mode = False, True
        eod_enabled, eod_hm = True, "15:20"
        exit_ob_enabled, exit_ob_ratio_min, exit_ob_ma_window = False, 0.4, 5
        exit_ob_min_profit_pct, exit_ob_min_hold_bars = 0.005, 3
        stop_ob_enabled, stop_ob_ratio_min, stop_ob_ma_window = False, 0.4, 5
        stop_ob_min_loss_pct, stop_ob_min_hold_bars = 0.003, 2
    finally:
        if own_db is not None:
            try:
                own_db.close()
            except Exception:
                pass

    return {
        "rsi_period": rsi_period,
        "mom_rsi_min": mom_rsi_min,
        "mom_rsi_max": mom_rsi_max,
        "mom_vol_mult": mom_vol_mult,
        "mom_vol_win": mom_vol_win,
        "mom_time_end_hm": mom_time_end,
        "e_min_chg_pct": e_min_chg_pct,
        "mom_max_from_open_pct": mom_max_open,
        "mom_min_from_open_pct": mom_min_open,
        "sl_pct": sl_pct,
        "tp_pct": tp_pct,
        "tp_max_pct": tp_max,
        "shoulder_min_high": shoulder_high,
        "shoulder_cut_pct": shoulder_cut,
        "ratchet_tiers": ratchet_tiers,
        "trail_pct": trail_pct,
        "trail_arm_pct": trail_arm_pct,
        "max_hold_bars": max_hold_bars,
        "max_daily": max_daily,
        "min_price": min_price,
        "max_daily_chg": max_daily_chg,
        "high_chase_thr": high_chase,
        "max_loss_krw": float(max_loss_krw),
        "min_drop_pct_for_loss_cut": min_drop_pct_for_loss_cut,
        "cooldown_min": cooldown_sec / 60.0,
        "time_start_hm": time_start,
        "time_end_hm": time_end,
        "skip_hts_scan_dupes": skip_hts,
        "use_defense_filters": use_defense,
        "use_high_chase_filter": use_high_chase_f,
        "use_daily_range_filter": use_daily_range_f,
        "use_rsi_filter": use_rsi_filter,
        **hts_flags,
        "slot_money": float(slot_money),
        "max_stocks": max_stocks,
        "total_budget_krw": float(total_budget),
        "min_hold_sec": min_hold_sec,
        "live_backtest_align": live_align,
        "live_signal_lookback_bars": lookback_bars,
        "live_align_use_forming_bar": use_forming,
        "cooldown_use_candle_floor": cd_floor,
        "cooldown_engine_only": cd_engine_only,
        "backtest_skip_pre_subscribe": skip_pre_sub,
        "eod_enabled": eod_enabled,
        "eod_hm": eod_hm,
        "force_eod_exit": eod_enabled,
        "portfolio_mode": portfolio_mode,
        "scan_interval_min": 1,
        # 수익구간 호가매도
        "exit_ob_enabled": exit_ob_enabled,
        "exit_ob_ratio_min": exit_ob_ratio_min,
        "exit_ob_ma_window": exit_ob_ma_window,
        "exit_ob_min_profit_pct": exit_ob_min_profit_pct,
        "exit_ob_min_hold_bars": exit_ob_min_hold_bars,
        # 손절호가
        "stop_ob_enabled": stop_ob_enabled,
        "stop_ob_ratio_min": stop_ob_ratio_min,
        "stop_ob_ma_window": stop_ob_ma_window,
        "stop_ob_min_loss_pct": stop_ob_min_loss_pct,
        "stop_ob_min_hold_bars": stop_ob_min_hold_bars,
    }


            _uef = request.args.get("use_ema_filter")
            params["use_ema_filter"] = (
                str(_uef).strip().lower() in ("1", "true", "y", "yes", "on")
                if _uef not in (None, "")
                else bool(_mom_eng.get("use_ema_filter", False))
            )

            _uef = request.args.get("use_ema_filter")
            params["use_ema_filter"] = (
                str(_uef).strip().lower() in ("1", "true", "y", "yes", "on")
                if _uef not in (None, "")
                else bool(_mom_eng.get("use_ema_filter", True))
            )

        "max_spread_pct": pickf(
            ("MOMENTUM_ORDERBOOK_MAX_SPREAD_PCT",),
            0.45,
        ),
        # 폼에 입력란 없음 — 실매/백테 엔진이 DB에서 직접 읽음. UI 안내용.
        "ob_min_bid_ask_ratio": pickf(("MOMENTUM_ORDERBOOK_MIN_BID_ASK_RATIO",), 1.0),
        "ob_ask_max_mult": pickf(("MOMENTUM_ORDERBOOK_ENTRY_ASK_MAX_MULT",), 50.0),
        "whipsaw_filter_enabled": pick_bool(("MOMENTUM_WHIPSAW_FILTER_ENABLED",), False),
        "whipsaw_subbar_sec": picki(("MOMENTUM_WHIPSAW_SUBBAR_SEC",), 30),
        "whipsaw_lookback_sec": picki(("MOMENTUM_WHIPSAW_LOOKBACK_SEC",), 60),
        "whipsaw_dip_pct": pickf(("MOMENTUM_WHIPSAW_DIP_PCT",), 0.005),
        "eod_enabled": pick_bool(("MOMENTUM_EOD_ENABLED",), True),
        "eod_hm": (
            str(env.get("MOMENTUM_EOD_HM") or "15:20").strip()
            if env.get("MOMENTUM_EOD_HM") not in (None, "", "None")
            else "15:20"
        ),
        **_momentum_exit_ui_from_engine(),
    }

        "max_spread_pct": pickf(
            ("MOMENTUM_ORDERBOOK_MAX_SPREAD_PCT",),
            0.45,
        ),
        "eod_enabled": pick_bool(("MOMENTUM_EOD_ENABLED",), True),
        "eod_hm": (
            str(env.get("MOMENTUM_EOD_HM") or "15:20").strip()
            if env.get("MOMENTUM_EOD_HM") not in (None, "", "None")
            else "15:20"
        ),
        **_momentum_exit_ui_from_engine(),
    }

  if (m.max_spread_pct != null) set('mom_max_spread_pct', m.max_spread_pct);
  // 호가 잔량·휩쏘는 입력란 없음 — DB값을 안내로만 표시 (백테/실매는 백단 get_env)
  const obExtra = [];
  if (m.ob_min_bid_ask_ratio != null) obExtra.push(`잔량≥${m.ob_min_bid_ask_ratio}`);
  if (m.ob_ask_max_mult != null) obExtra.push(`ask×≤${m.ob_ask_max_mult}`);
  if (m.whipsaw_filter_enabled) {
    obExtra.push(
      `휩쏘ON sub=${m.whipsaw_subbar_sec ?? '?'} lb=${m.whipsaw_lookback_sec ?? '?'} dip=${m.whipsaw_dip_pct ?? '?'}`,
    );
  } else if (m.whipsaw_filter_enabled === false) {
    obExtra.push('휩쏘OFF(DB)');
  }
  if ($('mom_ob_filter_note') && obExtra.length) {
    const prev = $('mom_ob_filter_note').textContent || '';
    $('mom_ob_filter_note').textContent =
      (prev ? prev + ' · ' : '') + 'DB상세: ' + obExtra.join(' · ');
  }
}

/** 페이지 로드·Optuna 적용 후 — 모멘텀 탭을 현재 DB로 다시 채움 */
async function reloadMomentumFormFromDb() {
  try {
    const r = await fetch('/api/env/params');
    const d = await r.json();
    if (d && d.momentum) fillMomentumFormFromApi(d.momentum);
  } catch (e) {
    console.warn('reloadMomentumFormFromDb', e);
  }
}

  if (m.max_spread_pct != null) set('mom_max_spread_pct', m.max_spread_pct);
}

      // 꼬리잡기 탭 — config_short + env_config (실매·파라서치와 동일)
      try { fillTailFormFromApi(d.tail || {}); } catch (e) { console.warn('fillTail', e); }

      // 더블BB 탭
      try { fillDbBandFormFromApi(d.dbband || {}); } catch (e) { console.warn('fillDbBand', e); }

      // 모멘텀 탭 — config_momentum + env_config (실패해도 다른 탭 fill 유지)
      try { fillMomentumFormFromApi(d.momentum || {}); } catch (e) { console.warn('fillMomentum', e); }
      try { fillUsMomentumFormFromApi(d.us_momentum || d.momentum || {}); } catch (e) { console.warn('fillUsMom', e); }

      // 돌파 탭 — DB(env_config BREAKOUT_*) = 백테 API _bo_defaults_from_db 와 동일
      try {
      const b = d.breakout || {};
      setM('bo_lookback', b.lookback_min);

      // 꼬리잡기 탭 — config_short + env_config (실매·파라서치와 동일)
      fillTailFormFromApi(d.tail || {});

      // 더블BB 탭
      fillDbBandFormFromApi(d.dbband || {});

      // 모멘텀 탭 — config_momentum + env_config
      fillMomentumFormFromApi(d.momentum || {});
      fillUsMomentumFormFromApi(d.us_momentum || d.momentum || {});

      // 돌파 탭 — DB(env_config BREAKOUT_*) = 백테 API _bo_defaults_from_db 와 동일
      const b = d.breakout || {};
      setM('bo_lookback', b.lookback_min);


      setM('rb_slot', rb.slot_money);
      if (rb.max_stocks != null) setM('rb_max_stocks', rb.max_stocks);
      if (rb.total_budget_krw != null) setM('rb_total_budget', rb.total_budget_krw);
      } catch (e) { console.warn('fillBreakout/range', e); }

      // 날짜(거래일) 확정 후 실거래 분석 1회 — 빈 날짜로 전체 조회·고스트 노출 방지
      loadActual();
    })
    .catch(() => {
      /* DB 연결 실패 시 HTML 기본값 유지 — 로컬 거래일로라도 조회 */
      loadActual();
    });

      setM('rb_slot', rb.slot_money);
      if (rb.max_stocks != null) setM('rb_max_stocks', rb.max_stocks);
      if (rb.total_budget_krw != null) setM('rb_total_budget', rb.total_budget_krw);

      // 날짜(거래일) 확정 후 실거래 분석 1회 — 빈 날짜로 전체 조회·고스트 노출 방지
      loadActual();
    })
    .catch(() => {
      /* DB 연결 실패 시 HTML 기본값 유지 — 로컬 거래일로라도 조회 */
      loadActual();
    });

    alert(
      `✅ ${j.strategy} 적용 완료 (upto=${j.upto || u})` +
      (j.env_id != null ? ` (env_id=${j.env_id})` : '') +
      `\ntrial #${m.optuna_trial_number ?? '—'} · PnL ${m.total_pnl} · WR ${m.win_rate}` +
      (keys.length ? `\n후처리키: ${keys.join(', ')}` : '') +
      `\n${j.note || ''}` +
      `\n→ 모멘텀 백테 탭 폼을 DB로 다시 채웁니다.`,
    );
    try { await reloadMomentumFormFromDb(); } catch (e2) { /* ignore */ }
  } catch (e) {
    alert('오류: ' + e);
  }
}

    alert(
      `✅ ${j.strategy} 적용 완료 (upto=${j.upto || u})` +
      (j.env_id != null ? ` (env_id=${j.env_id})` : '') +
      `\ntrial #${m.optuna_trial_number ?? '—'} · PnL ${m.total_pnl} · WR ${m.win_rate}` +
      (keys.length ? `\n후처리키: ${keys.join(', ')}` : '') +
      `\n${j.note || ''}`,
    );
  } catch (e) {
    alert('오류: ' + e);
  }
}

<script src="{{ url_for('static', filename='js/backtest.js') }}?v=20260822dbFormV4"></script>

<script src="{{ url_for('static', filename='js/backtest.js') }}?v=20260821wsApplyOnly"></script>


            <input class="form-check-input" type="checkbox" id="mom_use_ema_f">
            <label class="form-check-label" for="mom_use_ema_f" style="font-size:12px">EMA추세필터 (DB)</label>

            <input class="form-check-input" type="checkbox" id="mom_use_ema_f" checked>
            <label class="form-check-label" for="mom_use_ema_f" style="font-size:12px">EMA추세필터 (기본 ON)</label>

Shell

cd /home/hoon/kis_bot && python3 -m py_compile kis_trader/engine/momentum_engine.py && node --check static/js/backtest.js && python3 - <<'PY'
from kis_trader.utils.env import invalidate_merged_env_cache
invalidate_merged_env_cache()
from kis_trader.engine.momentum_engine import get_momentum_defaults_from_db
d=get_momentum_defaults_from_db()
assert d.get('chase_lookback_min')==12, d.get('chase_lookback_min')
assert d.get('use_ema_filter') is False, d.get('use_ema_filter')
assert d.get('ema_slow_period')==38, d.get('ema_slow_period')
assert abs(float(d.get('pullback_lookback_min'))-11)<0.01
print('OK defaults', {k:d[k] for k in ('use_ema_filter','chase_lookback_min','pullback_lookback_min','ema_slow','ema_slow_period','setup_vol_max_mult','mom_vol_mult')})
PY
sudo systemctl restart kis_backtest_web.service
sleep 1
systemctl is-active kis_backtest_web.service
# live validation required after engine change
TS=$(date +%Y%m%d_%H%M%S)
LOG=logs/test_live_execution_validation_${TS}.log
nohup python3 -u scripts/test_live_execution_validation.py > "$LOG" 2>&1 &
echo "VALID_PID=$! LOG=$LOG"

Verify defaults, restart web, start live smoke


cd /home/hoon/kis_bot && python3 - <<'PY'
from kis_trader.utils.env import invalidate_merged_env_cache
invalidate_merged_env_cache()
from kis_trader.engine.momentum_engine import get_momentum_defaults_from_db
d=get_momentum_defaults_from_db()
checks=[
 ('chase', d.get('chase_lookback_min'), 12),
 ('ema', d.get('use_ema_filter'), False),
 ('slow', d.get('ema_slow_period'), 38),
 ('pull', d.get('pullback_lookback_min'), 11),
 ('setup', d.get('setup_vol_max_mult'), 0.8),
]
for name,got,exp in checks:
    ok = got==exp or (isinstance(exp,float) and abs(float(got)-exp)<1e-9)
    print(('OK' if ok else 'FAIL'), name, got, 'expected', exp)
    assert ok
print('ALL_OK')
PY
# await validation briefly
sleep 15
rg -n "최종:|완결|FAIL|통과|실패" /home/hoon/kis_bot/logs/test_live_execution_validation_20260822_004627.log | tail -20
ps -p 333887 >/dev/null 2>&1 && echo STILL || echo DONE

Confirm V4 defaults; check validation log


맞습니다. 말씀하신 대로였고, **백테가 DB V4 타점을 안 읽던 구멍**이 있었습니다.

### 휩쏘·호가 폼 없는 값
| 항목 | 폼 | 실제 |
|------|-----|------|
| 호가 ON/OFF · 스프레드% | 있음 | 체크/입력 그대로 |
| 잔량비 · ask배수 · 휩쏘 sub/lb/dip | **없음** | **백단 DB(`get_env`)로 실매·백테 공통** |

→ 폼에 없어도 Optuna「100+휩쏘」적용값은 DB에 있고, 엔진이 그걸 씁니다. 이제 호가 안내줄에 `DB상세: 잔량≥… · 휩쏘ON …`도 보이게 했습니다.

### 진짜 버그 (그래서 백테가 이상했던 이유)
백그라운드 백테는 “DB 기준”인데, `get_momentum_defaults_from_db()`에 **추격/눌림/EMA/setup**이 **빠져 있었습니다.**
그래서 SL·vol 등은 DB, **EMA·chase는 HTML/코드 기본(ON·10분)** 으로 섞여 돌았습니다. 실매·Optuna와 다른 엔진이 된 겁니다.

### 고친 것
1. 엔진 기본값에 V4 키 DB 반영 (chase 12, EMA OFF, slow 38 … 확인됨)
2. 페이지 로드 fill이 중간에 깨져도 모멘텀 폼은 따로 채움
3. Optuna 적용 후 모멘텀 폼 **DB 재로드**
4. 웹 재시작 · 실매 스모크 **통과** (`logs/test_live_execution_validation_20260822_004627.log`)

하드 새로고침(`http://192.168.0.149:5050/`) 후 모멘텀 탭이 DB와 같은지 보시면 됩니다. 그다음 같은 기간 백테 한 번 더 돌리면 이번엔 mode 타점+호가 DB와 맞춰질 겁니다.


<timestamp>Saturday, Aug 22, 2026, 12:53 AM (UTC+9)</timestamp>
<user_query>
📈 KIS Quant Dashboard
모멘텀 완료 200/200
100% · best#199 WR 61.5% · PnL 40,882원 · PF 1.17 · MDD 78,449 · 52건
모멘텀 백테 완료 2026-08-18~2026-08-21
100%
업데이트: 2026. 8. 22. 오전 12:48:51
매매
모의 · active
시세:KIS(실) | 호가:키움(실) | DB:후보
📊 실거래 분석
📈 오늘 운영
⚙ 운영 설정
💼 보유·매도
📉 스캘핑 백테스트
🎣 꼬리잡기 백테스트
🚀 돌파매매 백테스트
🌊 모멘텀 백테스트
🇺🇸 해외 모멘텀
🔬 Optuna
📈 홀딩 전략
📦 박스엔진
📡 영구구독 관리
📢 DART 수주
🌊 모멘텀 전략 = SCALP(reversal)와 분리된 별도 추세추종 전략
• 진입: 키움 scalp 조건검색(갭상승+신고가+거래량 폭증) → 봇 자체 RSI 강세/거래량 spike/양봉 검증
• 청산: momentum_engine — 래칫/어깨·트레일 우선, 익절%는 tp_max 상한(마지막) · 손절/EOD
• 봇 ON: STRATEGY_MOMENTUM_ENABLED=true + CONDITION_MOMENTUM_NAME/SEQ 설정
• env 키: MOMENTUM_* 전용
시작일
2026-08-18
종료일
2026-08-21
1회투자(원)
MOMENTUM_SLOT_MONEY
300000
동시보유(종)
MOMENTUM_MAX_STOCKS
8
총운용한도(원)
MOMENTUM_TOTAL_BUDGET_KRW
1200000
※ 1회투자=포트폴리오 매수금(돌파 탭과 동일). 1회최대손실÷손절% 자동계산값(천만 등)과 다릅니다.
저장 후보 이력 사용 — 체크(기본) = DB 슬롯·후보 재현 (param_search_momentum 기본과 동일). 체크 해제 = 모멘텀 시뮬 유니버스 (--fallback-universe).
이력소스

키움 (target_candidates)
캔들

기본
시세(틱)

기본
호가

기본
📈 모멘텀 TRIGGER V4 (추격 패턴)
추격 패턴 — 하나라도 충족 시 진입 (env MOMENTUM_PATTERN_*)
N분 고가 돌파 + 양봉 + 거래량
스윙고점 → 눌림 → 재돌파
돌파관찰(분)
chase lookback · MOMENTUM_CHASE_LOOKBACK_MIN
12
눌림관찰(분)
pullback window · MOMENTUM_PULLBACK_LOOKBACK_MIN
11
눌림min(%)
pullback floor · MOMENTUM_PULLBACK_MIN_PCT
0.1
눌림max(%)
pullback cap · MOMENTUM_PULLBACK_MAX_PCT
5.2
눌림Vol×
MOMENTUM_SETUP_VOL_MAX_MULT
0.8
눌림음봉min
MOMENTUM_SETUP_BEAR_BARS_MIN
2
RSI min
RSI floor · MOMENTUM_RSI_MIN
50
RSI max
RSI cap · MOMENTUM_RSI_MAX
80
RSI 상한필터 (기본 OFF)
거래량배수
vol spike · MOMENTUM_VOL_MULT
3.3000000000000003
관찰봉수
vol window · MOMENTUM_VOL_WIN
8
전일종가+%(%)
HTS K · MOMENTUM_E_MIN_CHG_PCT
1.4000000000000001
매수종료
buy cutoff · MOMENTUM_TIME_END_HM
1530
ℹ V4 TRIGGER: 돌파 OR 눌림재돌파 + RSI 하한 + 거래량 + (선택) EMA 추세. 청산은 V4 어깨·트레일·손절 순. 고점추격·시가과열·RSI상한은 기본 OFF — 필요 시 아래 방어 체크박스에서 켜세요.
레거시 시가컷: mom_max_from_open=999 이면 비활성.
시가+과열(%)
legacy · MOMENTUM_MAX_FROM_OPEN_PCT
49
시가+약세(%)
legacy · MOMENTUM_MIN_FROM_OPEN_PCT
-999
🛑 청산 / 공통 (momentum_engine 전용)
청산 순서: 래칫/어깨 → 트레일 → 손절 → 시간컷 → 금액손실컷 → 익절(tp_max 상한) → 장마감 (실매 MomentumStrategy · param_search 와 동일. SCALP reversal V자 청산과 별도)

손절(%)
stop-loss % · MOMENTUM_STOP_LOSS_PCT
5.9
익절(%)
take-profit cap · MOMENTUM_TAKE_PROFIT_PCT
8.1
익절상한%
tp max cap · MOMENTUM_TP_MAX_PCT
15.5
어깨발동%
shoulder arm · MOMENTUM_SHOULDER_MIN_HIGH_PCT
2.6
어깨폭%
shoulder cut · MOMENTUM_SHOULDER_CUT_PCT
0.15
트레일%
trail cut · MOMENTUM_TRAIL_PCT
2.3
트레일무장%
trail arm · MOMENTUM_TRAIL_ARM_PCT
3.6
최대보유(봉)
time cut · MOMENTUM_MAX_HOLD_BARS
260
래칫티어
ratchet tiers · MOMENTUM_RATCHET_TIERS
4:1.5,8:0.5,14:3
쿨다운(분)
re-entry cooldown · MOMENTUM_COOLDOWN_SEC
5
매수시작
buy start HHMM · MOMENTUM_TIME_START
900
일일최대매수
max entries/day · MOMENTUM_MAX_DAILY
75
EOD청산
구독전분제외
MOMENTUM_BACKTEST_SKIP_PRE_SUBSCRIBE
EOD시각
MOMENTUM_EOD_HM
15:20
방어로직
entry filters · MOMENTUM_USE_DEFENSE_FILTERS
진입 방어 ON
TRIGGER 필터(백테)
호가·프로그램 · 끄고/켜고 비교용
호가필터 ON
글로벌 OFF · 실매 ON · 모멘텀 전용 설정값 · DB상세: 잔량≥1.21 · ask×≤65.2 · 휩쏘ON sub=13 lb=102 dip=0.0048
프로그램필터 ON
글로벌 OFF · 실매 OFF · 모멘텀 전용 설정값
스프레드상한(%)
MOMENTUM_ORDERBOOK_MAX_SPREAD_PCT
6.3
고점추격필터 (기본 OFF)
당일변동필터 (기본 OFF)
EMA추세필터 (DB)
EMA빠름
fast period · MOMENTUM_EMA_FAST_PERIOD
9
EMA느림
slow period · MOMENTUM_EMA_SLOW_PERIOD
38
고점추격방지
near-high gate · HIGH_CHASE_THR
0.96
급등주필터(%)
intraday range cap · MAX_DAILY_CHG
50
최소가격(원)
min tick price · MOMENTUM_MIN_PRICE
1000
1회최대손실(원)
loss cap KRW · MOMENTUM_MAX_LOSS_PER_TRADE_KRW
200000
본절마진(%)
breakeven buffer · MOMENTUM_MIN_PROFIT_PCT
0.7000000000000001
📅 다단트레일 ON이면 당일 실현손익 고점 대비 되돌릴 때 신규매수만 중단. 리스크버짓(B) ON이면 hit 후 보유최악손절합>cushion 이면 전량정리. DB=MOMENTUM_DAILY_PROFIT_*.
다단트레일
리스크버짓(B)
다단규칙(수익원:컷%,…)
프리셋 선택…(직접입력)
25000:40,50000:30,100000:20
단일되돌림(%)
0
발동최소수익(원)
0
방식
trailing
저장 후보 이력 — 체크: DB 슬롯·후보 / 해제: 시뮬 유니버스 (이력소스 셀렉트는 위 포트폴리오 카드)
env 타임라인 — 체크: 그 시각 DB설정 재현 / 해제(기본): 폼 파람으로 구간 전체
탐색: CLI · 전역 1잡
done 100% 2026-08-18~2026-08-21 · 완료 trades=28 PnL=43046 · /home/hoon/kis_bot/logs/momentum_bt_20260818_20260821_20260822_004905.log · 전역 1잡 · 탭 이동 OK
유니버스: 저장 이력 1878슬롯·strict 돌파+눌림(12분) | RSI≥50 | 거래량×3.3000000000000003 | 손절-5.9% | 어깨(2.6%/0.15%)·래칫4:1.5,8:0.5,14:3 | 익절상한+8.1% | 2026-08-18~2026-08-21 | EMA OFF | 1회 300,000원 · 동시8종 · 한도 1,200,000원 | 쿨다운5분 | 일75회 | 종목수 628개
💰 총한도 1,200,000원 < 동시8×1회투자 2,400,000원 — 잔여금 소액매수·과다 회전 위험. 실매 정렬: 총한도↑ 또는 동시보유↓
📊 ws_ticks 1,275,543건 · 분봉커버 구독구간 8.05% · 거래종목 26.56%(21종) · 전체 6.05% · 틱종목 6.07% (620/628종) | 진입 틱—/시가28 · 청산 틱17/OHLC0
🔄 매수큐 실매형 10초 · 스캔9120 · 매수28 | 유니버스 스캔시각·디바운스30초
💡 장중 누적 최고 +43,046원 (2026-08-21 13:58:06) · 최종 43,046원
총 거래
28건
승률
64.29%
순손익(수수료·세금 포함)
43,046원
Profit Factor
1.39
최대 낙폭(MDD)
-56,181원
평균 보유(분)
507.4분
총 수익률(운용한도 대비)
+3.59%
일평균 수익률
+0.897%
🟢 승 18건 (64.29%)
🔴 패 10건 (35.7%)
누적 손익 곡선 (가상)
매도 이유 분포
일별 손익 (가상)
가상 거래 내역 (최근 200건)
📋 백테 MOMENTUM · 한도 1,200,000원 | 장중 누적 최고 +43,046원 (2026-08-21 13:58:06) | 최종 누적 43,046원
💰 총한도 1,200,000원 < 동시8×1회투자 2,400,000원 — 잔여금 소액매수·과다 회전 위험. 실매 정렬: 총한도↑ 또는 동시보유↓
📊 ws_ticks 1,275,543건 · 분봉커버 구독구간 8.05% · 거래종목 26.56%(21종) · 전체 6.05% · 틱종목 6.07% (620/628종) | 진입 틱—/시가28 · 청산 틱17/OHLC0
🔄 매수큐 실매형 10초 · 스캔9120 · 매수28 | 유니버스 스캔시각·디바운스30초
정렬
종목	매수시각	매도시각	매수가	매도가	수량	손익(원)	수익률%	누적손익	누적%	보유(분)	체결디버그	매수호가
녹OK · 노랑참고 · 빨강나쁨	매도호가
녹OK · 노랑참고 · 빨강나쁨	매도사유
294570
294570
2026-08-21 13:01:00	2026-08-21 13:58:06	25,050	25,350	11	2,715	+1.20%	43,046	+3.59%	57.1	-→틱청산	기록없음
체결≠호가저장	기록없음	어깨컷
294570
294570
2026-08-21 11:04:00	2026-08-21 12:54:48	24,250	24,900	12	7,174	+2.68%	40,331	+3.36%	110.8	-→틱청산	괜찮음
가격간격 OK 0.21% (6.30%까지)
살 힘 OK 2.12 (1.21 이상)
사겠음 1,302주 · 팔겠음 614주
시차 +69s · ls_uh1	기록없음	어깨컷
폴라리스AI
039980
2026-08-21 11:23:06	2026-08-21 11:37:11	5,730	5,970	52	11,830	+4.19%	33,157	+2.76%	14.1	-→틱청산	기록없음
체결≠호가저장	수급약함·근처
OR 약함 0.30 (총사겠음÷총팔겠음, 기준 0.33)
가격간격 OK 0.33%
총사겠음 14,878 · 총팔겠음 49,550
시차 +349s · ls_uh1	어깨컷
틱공백EOD갤럭시아머니트리
094480
2026-08-21 10:44:01	2026-08-21 11:05:00	5,600	6,070	53	24,238	+8.39%	21,327	+1.78%	21	-→틱공백EOD	기록없음
체결≠호가저장	수급OK
OR OK 1.77 (총사겠음÷총팔겠음, 기준 0.33)
가격간격 OK 0.17%
총사겠음 55,376 · 총팔겠음 31,318
시차 +41s · ls_uh1	익절
008260
008260
2026-08-20 11:19:04	2026-08-21 09:00:24	3,445	3,445	87	-629	+0.00%	-2,911	-0.24%	1301.3	-→틱청산	괜찮음·근처
가격간격 OK 0.14% (6.30%까지)
살 힘 OK 11.48 (1.21 이상)
사겠음 8,360주 · 팔겠음 728주
시차 +887s · ls_uh1	수급OK
OR OK 0.50 (총사겠음÷총팔겠음, 기준 0.33)
가격간격 OK 0.14%
총사겠음 1,017 · 총팔겠음 2,016
시차 +0s · kiwoom_0d	시간컷
나이벡
138610
2026-08-20 15:05:52	2026-08-21 09:00:01	19,010	18,100	15	-14,222	-4.79%	-2,282	-0.19%	1074.2	-→틱청산	괜찮음
가격간격 OK 0.26% (6.30%까지)
살 힘 OK 2.97 (1.21 이상)
사겠음 431주 · 팔겠음 145주
시차 +0s · filter_eval	수급OK
OR OK 0.93 (총사겠음÷총팔겠음, 기준 0.33)
가격간격 OK 0.22%
총사겠음 162 · 총팔겠음 174
시차 +0s · kiwoom_0d	시간컷
틱공백EOD엔알비
475230
2026-08-20 12:46:00	2026-08-21 09:00:00	9,430	9,520	31	2,171	+0.95%	11,940	+1.00%	1214	-→틱공백EOD	기록없음
체결≠호가저장	기록없음	시간컷
틱공백EOD갤럭시아머니트리
094480
2026-08-20 11:37:03	2026-08-21 09:00:00	5,390	5,750	55	19,139	+6.68%	9,769	+0.81%	1283	-→틱공백EOD	팔대기많음
가격간격 OK 0.19% (6.30%까지)
살 힘 OK 1.57 (1.21 이상)
사겠음 5,142주 · 팔겠음 3,270주
시차 +0s · filter_eval	기록없음	시간컷
핑거
163730
2026-08-20 10:34:27	2026-08-20 14:55:00	10,590	10,430	28	-5,094	-1.51%	-9,370	-0.78%	260.6	-→틱청산	기록없음
체결≠호가저장	기록없음	시간컷
레이언스
228850
2026-08-20 12:16:39	2026-08-20 12:42:01	6,910	7,050	43	5,384	+2.03%	-4,276	-0.36%	25.4	-→틱청산	살사람적음
가격간격 OK 0.14% (6.30%까지)
살 힘 약함 0.10 (1.21 이상)
사겠음 732주 · 팔겠음 7,006주
시차 +1s · filter_eval	수급OK
OR OK 1.16 (총사겠음÷총팔겠음, 기준 0.33)
가격간격 OK 0.71%
총사겠음 12,732 · 총팔겠음 11,016
시차 +10s · ls_uh1	어깨컷
레이언스
228850
2026-08-20 10:29:01	2026-08-20 10:59:00	6,630	6,790	45	6,559	+2.41%	-9,660	-0.80%	30	-→틱청산	괜찮음
가격간격 OK 0.32% (6.30%까지)
살 힘 OK 18.32 (1.21 이상)
사겠음 5,275주 · 팔겠음 288주
시차 +49s · filter_eval	기록없음	어깨컷
마크로젠
038290
2026-08-20 10:29:10	2026-08-20 10:33:03	13,800	12,910	21	-19,262	-6.45%	-16,219	-1.35%	3.9	-→틱청산	괜찮음·근처
가격간격 OK 0.39% (6.30%까지)
살 힘 OK 1.73 (1.21 이상)
사겠음 38주 · 팔겠음 22주
시차 +502s · filter_eval	수급약함·근처
OR 약함 0.24 (총사겠음÷총팔겠음, 기준 0.33)
가격간격 OK 0.39%
총사겠음 263 · 총팔겠음 1,098
시차 +735s · filter_eval	손절
골프존홀딩스
121440
2026-08-19 15:08:00	2026-08-20 09:00:30	7,870	7,980	38	3,544	+1.40%	3,043	+0.25%	1072.5	-→틱청산	괜찮음
가격간격 OK 0.38% (6.30%까지)
살 힘 OK 1.75 (1.21 이상)
사겠음 1,373주 · 팔겠음 787주
시차 +17s · filter_eval	수급OK
OR OK 29.07 (총사겠음÷총팔겠음, 기준 0.33)
가격간격 OK 0.13%
총사겠음 14,708 · 총팔겠음 506
시차 +0s · kiwoom_0d	시간컷
틱공백EOD대화제약
067080
2026-08-19 14:52:59	2026-08-20 09:00:00	10,460	11,020	28	15,034	+5.35%	-501	-0.04%	1087	-→틱공백EOD	기록없음
체결≠호가저장	기록없음	시간컷
틱공백EOD에스넷
038680
2026-08-19 13:06:58	2026-08-20 09:00:00	4,330	4,235	69	-7,170	-2.19%	-15,535	-1.29%	1193	-→틱공백EOD	기록없음
체결≠호가저장	기록없음	시간컷
틱공백EOD아이패밀리에스씨
114840
2026-08-19 11:52:00	2026-08-20 09:00:00	9,310	9,330	32	13	+0.21%	-8,365	-0.70%	1268	-→틱공백EOD	괜찮음
가격간격 OK 0.11% (6.30%까지)
살 힘 OK 2.49 (1.21 이상)
사겠음 167주 · 팔겠음 67주
시차 +39s · filter_eval	기록없음	시간컷
위메이드맥스
101730
2026-08-19 11:37:30	2026-08-19 14:26:58	4,630	4,775	64	8,640	+3.13%	-8,378	-0.70%	169.5	-→틱청산	괜찮음·근처
가격간격 OK 0.11% (6.30%까지)
살 힘 OK 3.01 (1.21 이상)
사겠음 1,582주 · 팔겠음 525주
시차 +325s · filter_eval	수급약함·근처
OR 약함 0.26 (총사겠음÷총팔겠음, 기준 0.33)
가격간격 OK 0.22%
총사겠음 1,316 · 총팔겠음 5,168
시차 +601s · kiwoom_0d	어깨컷
대화제약
067080
2026-08-19 09:01:50	2026-08-19 13:22:38	10,500	10,530	28	221	+0.29%	-17,018	-1.42%	260.8	-→틱청산	살힘약함
가격간격 OK 0.28% (6.30%까지)
살 힘 약함 0.95 (1.21 이상)
사겠음 190주 · 팔겠음 199주
시차 +0s · filter_eval	기록없음	시간컷
위메이드맥스
101730
2026-08-19 11:11:00	2026-08-19 11:13:13	4,515	4,620	66	6,291	+2.33%	-17,239	-1.44%	2.2	-→틱청산	살힘약함·근처
가격간격 OK 0.34% (6.30%까지)
살 힘 약함 0.75 (1.21 이상)
사겠음 960주 · 팔겠음 1,284주
시차 +660s · kiwoom_0d	수급OK·근처
OR OK 1.37 (총사겠음÷총팔겠음, 기준 0.33)
가격간격 OK 0.34%
총사겠음 4,177 · 총팔겠음 3,047
시차 +793s · kiwoom_0d	어깨컷
골프존홀딩스
121440
2026-08-19 10:56:06	2026-08-19 11:12:15	7,700	7,950	38	8,867	+3.25%	-23,530	-1.96%	16.1	-→틱청산	팔대기많음
가격간격 OK 0.39% (6.30%까지)
살 힘 OK 2.69 (1.21 이상)
사겠음 4,833주 · 팔겠음 1,796주
시차 +21s · filter_eval	수급OK·근처
OR OK 0.93 (총사겠음÷총팔겠음, 기준 0.33)
가격간격 OK 0.26%
총사겠음 8,433 · 총팔겠음 9,042
시차 +854s · filter_eval	어깨컷
위메이드맥스
101730
2026-08-19 10:16:00	2026-08-19 10:52:10	4,295	4,605	69	20,726	+7.22%	-32,397	-2.70%	36.2	-→틱청산	괜찮음·근처
가격간격 OK 0.24% (6.30%까지)
살 힘 OK 5.08 (1.21 이상)
사겠음 7,508주 · 팔겠음 1,479주
시차 +885s · kiwoom_0d	수급OK·근처
OR OK 1.32 (총사겠음÷총팔겠음, 기준 0.33)
가격간격 OK 0.11%
총사겠음 3,839 · 총팔겠음 2,908
시차 +452s · kiwoom_0d	어깨컷
틱공백EOD화신
010690
2026-08-19 09:43:50	2026-08-19 09:50:00	10,030	9,410	29	-18,556	-6.18%	-53,123	-4.43%	6.2	-→틱공백EOD	괜찮음
가격간격 OK 0.10% (6.30%까지)
살 힘 OK 2.50 (1.21 이상)
사겠음 2,621주 · 팔겠음 1,047주
시차 +0s · filter_eval	수급OK
OR OK 6.12 (총사겠음÷총팔겠음, 기준 0.33)
가격간격 OK 0.21%
총사겠음 13,862 · 총팔겠음 2,264
시차 +100s · kiwoom_0d	손절
아난티
025980
2026-08-19 09:01:00	2026-08-19 09:03:00	5,000	5,150	60	8,352	+3.00%	-34,567	-2.88%	2	-→틱청산	팔대기많음
가격간격 OK 0.19% (6.30%까지)
살 힘 OK 3.03 (1.21 이상)
사겠음 96,575주 · 팔겠음 31,908주
시차 +172s · filter_eval	수급OK
OR OK 0.82 (총사겠음÷총팔겠음, 기준 0.33)
가격간격 OK 0.19%
총사겠음 52,830 · 총팔겠음 64,736
시차 +0s · kiwoom_0d	래칫컷
틱공백EOD덕성
004830
2026-08-18 13:43:00	2026-08-19 09:00:00	3,475	3,340	86	-12,215	-3.88%	-42,919	-3.58%	1157	-→틱공백EOD	괜찮음·근처
가격간격 OK 0.14% (6.30%까지)
살 힘 OK 3.11 (1.21 이상)
사겠음 747주 · 팔겠음 240주
시차 +524s · filter_eval	기록없음	시간컷
틱공백EOD한세실업
105630
2026-08-18 12:56:00	2026-08-19 09:00:00	9,820	9,560	30	-8,403	-2.65%	-30,704	-2.56%	1204	-→틱공백EOD	살힘약함
가격간격 OK 0.10% (6.30%까지)
살 힘 약함 0.12 (1.21 이상)
사겠음 37주 · 팔겠음 304주
시차 +80s · kiwoom_0d	기록없음	시간컷
틱공백EOD양지사
030960
2026-08-18 12:51:00	2026-08-19 09:00:00	4,750	4,660	63	-6,287	-1.89%	-22,301	-1.86%	1209	-→틱공백EOD	괜찮음·근처
가격간격 OK 0.21% (6.30%까지)
살 힘 OK 2.98 (1.21 이상)
사겠음 2,023주 · 팔겠음 680주
시차 +269s · kiwoom_0d	기록없음	시간컷
틱공백EOD워트
396470
2026-08-18 13:06:40	2026-08-18 15:11:00	7,120	6,680	42	-19,072	-6.18%	-16,014	-1.33%	124.3	-→틱공백EOD	괜찮음
가격간격 OK 0.28% (6.30%까지)
살 힘 OK 3.00 (1.21 이상)
사겠음 3,415주 · 팔겠음 1,138주
시차 +1s · kiwoom_0d	수급OK
OR OK 1.27 (총사겠음÷총팔겠음, 기준 0.33)
가격간격 OK 0.14%
총사겠음 5,562 · 총팔겠음 4,384
시차 +80s · kiwoom_0d	손절
제너셈
217190
2026-08-18 13:12:02	2026-08-18 13:16:01	7,260	7,350	41	3,058	+1.24%	3,058	+0.25%	4	-→틱청산	살힘약함
가격간격 OK 0.14% (6.30%까지)
살 힘 약함 0.97 (1.21 이상)
사겠음 1,695주 · 팔겠음 1,750주
시차 +42s · kiwoom_0d	수급OK
OR OK 0.72 (총사겠음÷총팔겠음, 기준 0.33)
가격간격 OK 0.14%
총사겠음 7,119 · 총팔겠음 9,900
시차 +35s · kiwoom_0d	어깨컷
파라미터 그리드 탐색은 CLI param_search_momentum.py — 웹은 페이지 로드 시 /api/env/params → config_momentum + env_config 병합값으로 입력란 자동 채움.

결과가 다르네 왜 더 높게 나왔지 그리고 왜 하루만 좋아? 그리고 매도 익정손절란이 안맞는게 많은데?
</user_query>


결과·일별·호가/매도사유 불일치를 로그·거래 JSON으로 짧게 대조합니다.

cd /home/hoon/kis_bot && python3 - <<'PY'
import json
from collections import defaultdict
from pathlib import Path
r=json.loads(Path('kis_trader/backtest/results/momentum_bt_20260818_20260821_20260822_004905.json').read_text())
trades=r.get('trades') or []
print('n', len(trades), 'pnl', r.get('summary',{}).get('total_pnl'), 'wr', r.get('summary',{}).get('win_rate'))
p=r.get('params') or {}
print('ema', p.get('use_ema_filter'), 'chase', p.get('chase_lookback_min'), 'ob', p.get('_orderbook_filter_enabled'), 'sl', p.get('sl_pct'))
# daily pnl
by=defaultdict(lambda:{'n':0,'pnl':0.0,'w':0})
reasons=defaultdict(int)
for t in trades:
    sell=str(t.get('sell_date') or t.get('sell_time') or '')[:10]
    day=sell.replace('-','')[:8] if sell else '?'
    if len(day)<8:
        # try candle
        st=str(t.get('sell_dt') or t.get('exit_time') or '')
        day=st.replace('-','')[:8] or '?'
    pnl=float(t.get('pnl') or t.get('realized_pnl') or t.get('profit') or 0)
    by[day]['n']+=1; by[day]['pnl']+=pnl
    if pnl>0: by[day]['w']+=1
    reasons[str(t.get('reason') or t.get('sell_reason') or '?')]+=1
print('--- daily ---')
for d in sorted(by):
    x=by[d]
    print(d, 'n=',x['n'], 'pnl=', round(x['pnl']), 'wins=',x['w'])
print('--- reasons ---')
for k,v in sorted(reasons.items(), key=lambda kv:-kv[1]):
    print(v, k)
# compare mode e room vs this
opt=json.loads(Path('kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json').read_text())
mc=opt.get('mode_combo') or {}
print('mode_combo BT', (mc.get('backtest') or {}))
# gated 199
for row in (opt.get('results_gated') or []):
    if int(row.get('optuna_trial_number') or -1)==199:
        print('optuna#199', row.get('total_trades'), row.get('win_rate'), row.get('total_pnl'))
        break
PY

Daily PnL and reason breakdown from latest BT


폼·DB는 이제 맞게 보입니다. 숫자가 다른 이유와 “하루만 좋은” 이유는 아래입니다.

### 왜 Optuna 표랑 다르고 더 높게 나왔나
비교 대상이 **서로 다른 실험**입니다.

| | 거래 | PnL | 조건 |
|--|------|-----|------|
| Optuna **#199** | 52 | +40,882 | 차트만 · **호가 OFF** |
| mode 차트 | 57 | +14,850 | 최빈 조립 · **호가 OFF** |
| mode **100방** | 9/24 | +23,337 | **이미 난 체결에 호가만 재필터** |
| **이번 웹백테** | 28 | **+43,046** | mode타점 + **호가ON + 휩쏘** · 포트폴리오 전체 재시뮬 |

그래서 100방 +2.3만과 같을 수 없고, #199와 PnL만 비슷한 건 **우연에 가깝습니다**(거래 구성·건수가 다름).

### 왜 하루만 좋아 보이나
청산일 기준 일별:

| 날 | 건수 | PnL |
|----|------|-----|
| 8/18 | 2 | **-16,014** |
| 8/19 | 10 | +7,636 |
| 8/20 | 8 | -992 |
| **8/21** | 8 | **+52,416** |

최종 +4.3만이 거의 **8/21 하루**(갤럭시아 익절·폴라리스·294570 등)에서 나옵니다. 4일 중 하루 잭팟형이라 “하루만 좋다”가 맞습니다. 과적합·표본 짧은 장 경고와 같은 그림입니다.

### 매도사유·호가가 안 맞아 보이는 이유
버그라기보다 **칸 의미가 다름**인 경우가 많습니다.

1. **체결디버그 `틱공백EOD` + 사유 `시간컷`**
   보유 중 틱이 비어서 EOD/시가로 정리한 건데, 사유 라벨은 시간컷/EOD 계열로 남는 표시입니다. “손절인데 익절로 찍힘” 수준이 아니라 **청산 경로 표기 이중**에 가깝습니다.

2. **매수호가 `기록없음` / `체결≠호가저장`**
   그 시각 DB 호가스냅이 없음(스냅 미스). 필터 ON이어도 스냅 없으면 판정·저장이 비고, 매도 쪽만 나중에 스냅이 있어 “매도호가만 OK”처럼 보입니다.

3. **매수 OK · 매도 OR 약함**
   진입 시각 호가 ≠ 청산 시각 호가입니다. 같은 종목이어도 정상이고, 익절/어깨와 무관합니다.

---

**한 줄:** 지금은 DB·폼·백테 V4는 맞춰졌고, +4.3만은 “100방 성적 재현”이 아니라 **호가+휩쏘 켠 전체 백테가 8/21에 크게 먹은 결과**입니다. 적용 판단은 #199/다일 재검증 쪽을 보는 게 맞고, 100방 숫자는 참고용 필터 성적입니다.


<timestamp>Saturday, Aug 22, 2026, 12:58 AM (UTC+9)</timestamp>
<user_query>
왜 호가온이 기본이야? 그리고 타점이 그게 맞아? 옵투나는 후처리고 그니까 우선순위가 같냐고
</user_query>


호가 ON 기본값 출처와 Optuna(탐색→후처리) vs 실매/백테 우선순위를 코드에서 확인합니다.


### 1. 왜 호가 ON이 기본이냐
두 겹입니다.

1. **HTML**에 `mom_ob_filter`가 **처음부터 `checked`** 로 박혀 있음 (로드 전·실패 시에도 ON처럼 보임).
2. 지금은 Optuna **mode「100 진입」적용**으로 DB `MOMENTUM_ORDERBOOK_FILTER_ENABLED=true`라서, 맞추려면 ON이 맞음.

Optuna **학습(trial)** 기본은 원래 **호가 OFF**입니다.
「호가 ON = 전략 기본」이 아니라, **후처리 방 적용 후·HTML 기본 체크** 때문에 ON으로 보이는 겁니다.

---

### 2. 타점이 그게 맞냐
지금 폼( chase 12 · vol 3.3 · SL 5.9 · EMA OFF … )은 **mode_combo 타점**이고, **#199(+40,882) 타점이 아닙니다.**

| | 타점 | 호가 |
|--|------|------|
| #199 | trial 파라미터 | 탐색 중 OFF |
| mode | 축별 최빈 조립 | 적용 전 OFF |
| 지금 DB/폼 | **mode** | **100방 ON (+휩쏘)** |

mode「100」을 누르면 → **mode 차트 + 그 방 호가**가 맞습니다. #199를 쓰려면 gated #199「타점만(호가OFF)」를 따로 적용해야 합니다.

---

### 3. Optuna 후처리랑 우선순위가 같냐
**역할은 같고, 숫자 의미는 다릅니다.**

실매·웹백테(적용 후) 진입 순서:
1. **차트/TRIGGER 타점**
2. **휩쏘** (켜져 있으면)
3. **진입 호가** (켜져 있으면)
4. 프로그램 등

Optuna:
1. **탐색** = 타점만 (호가 OFF) → 랭킹·#199
2. **후처리 8방** = 이미 난 체결에 호가/휩쏘를 **다시 씌운 참고 성적** (전체 백테 재실행이 아님)

그래서
- 우선순위(타점 → 휩쏘 → 호가)는 **실매·백테·후처리 시뮬이 같은 축**이고
- 「100방 +2.3만」= 그 순서의 **필터 성적**이지, Optuna 1위 타점 그 자체가 아닙니다.

**정리:** 호가 ON은 “원래 기본”이 아니라 **HTML 체크 + 100방 적용** 때문이고, 지금 타점은 **mode가 맞고 #199는 아님**. 후처리는 같은 필터 순서의 **참고 레이어**이지, trial 순위를 바꾸는 본탐색이 아닙니다.


<timestamp>Saturday, Aug 22, 2026, 1:01 AM (UTC+9)</timestamp>
<user_query>
근데 왜 똑같은 틱 호가 데이터로 돌리는데 옵투나랑 백테가 결과가 다르냐 이말이야
</user_query>


If the available MCP tools do not fully support what the user asked you to do, complete the work you can with the current tool set. In your work summary, include what you were unable to do with MCP and why. Do not use browser automation to work around missing or unavailable MCP tools unless the user explicitly asks you to use the browser.


같은 틱·호가 **파일/DB**를 써도, **무엇을 켠 채로·어떤 파이프로** 돌리느냐가 달라서 결과가 갈립니다.

cd /home/hoon/kis_bot && python3 - <<'PY'
# 한눈에: 세 숫자 조건
print('''
[같은 데이터]
  캔들·ws_ticks·호가 테이블 = 공유

[다른 시뮬]
  Optuna trial(#199)     : 타점탐색 · 호가OFF · 휩쏘탐색축 아님 · 52건 +40882
  Optuna mode 차트       : 최빈조립 타점 · 호가OFF · 57건 +14850
  Optuna 후처리 100방    : 위 체결에 호가만 재필터 · 전체 재시뮬 아님 · 9건 +23337
  웹백테(지금)           : mode타점 · 호가ON · 휩쏘ON · 포트 전체 재시뮬 · 28건 +43046
''')
PY

Summarize why same data differs


**데이터는 같아도, 시뮬 스위치가 다릅니다.** 틱·호가 DB를 같이 읽는 것과 “같은 실험”은 별개입니다.

### 핵심
Optuna **학습 trial**은 기본이 **호가 OFF**입니다.
호가 테이블이 있어도 **진입 판정에 안 씁니다.**
웹백테(지금)는 **호가 ON + 휩쏘 ON**이라, 같은 스냅을 **걸러 쓰는** 쪽입니다.

그래서:
- 같은 틱으로 청산해도
- **누가 들어가느냐**가 달라지고
- 동시보유·한도 때문에 뒤에 거래가 연쇄로 갈라집니다.
→ 건수·PnL이 달라지는 게 정상입니다.

### 숫자로 보면
| 돌린 것 | 호가/휩쏘 | 무엇을 하냐 | 결과 |
|---------|-----------|-------------|------|
| Optuna #199 | **OFF** | 전체 포트 시뮬 | 52건 +4.0만 |
| mode 차트 | **OFF** | 전체 포트 시뮬 | 57건 +1.4만 |
| 후처리 100방 | 필터만 | **이미 난 체결에 호가만 재적용** (재시뮬 아님) | 9건 +2.3만 |
| 웹백테 | **ON+휩쏘** | 전체 포트 **다시** 시뮬 | 28건 +4.3만 |

후처리 100방과 웹백테가 같아야 한다고 생각하면 틀린 기대입니다.
후처리는 “그 체결들만 호가로 걸렀을 때”, 웹은 “처음부터 호가 켠 채로 장 전체를 다시 돌린 것”입니다.

### 한 줄
**같은 원천 데이터 ≠ 같은 ON/OFF·같은 파이프.**
맞춰 보고 싶으면 Optuna와 동일하게 **호가 OFF·같은 타점(#199 또는 mode)** 으로 웹백테 1회 돌리면, 그때야 “정합 비교”가 됩니다.


<timestamp>Saturday, Aug 22, 2026, 1:03 AM (UTC+9)</timestamp>
<user_query>
정합 비교만 좀 해줘봐 디비값 바꾸기 싫으니까
</user_query>


DB는 건드리지 않고, Optuna와 같은 타점·호가 OFF로 웹/CLI 백테만 맞춰 비교하겠습니다.


cd /home/hoon/kis_bot && python3 - <<'PY'
import json
from pathlib import Path
p = Path('kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json')
d = json.loads(p.read_text())
print('top keys', sorted(d.keys())[:40])
for k in ('best','best_trial','mode_combo','mode','start','end','study_name','meta','summary'):
    if k in d:
        v=d[k]
        print(k, type(v).__name__, (str(v)[:200] if not isinstance(v, dict) else list(v.keys())[:20]))
# find trial 199
trials = d.get('trials') or d.get('top_trials') or d.get('results') or []
print('trials type', type(trials), 'len', len(trials) if hasattr(trials,'__len__') else None)
if isinstance(trials, list) and trials:
    print('trial0 keys', trials[0].keys() if isinstance(trials[0], dict) else type(trials[0]))
# search number 199
found=None
if isinstance(trials, list):
    for t in trials:
        if not isinstance(t, dict):
            continue
        n = t.get('number', t.get('trial', t.get('trial_number')))
        if n == 199:
            found=t
            break
print('found199', bool(found))
if found:
    print({k: found.get(k) for k in ('number','value','pnl','trades','n_trades','params') if k in found or k=='params'})
    params = found.get('params') or found.get('param') or {}
    if isinstance(params, dict):
        print('n_params', len(params))
# mode_combo
mc = d.get('mode_combo') or d.get('mode') or {}
if isinstance(mc, dict):
    print('mode_combo keys', list(mc.keys())[:30])
    for k in ('pnl','trades','n_trades','params','stats','result'):
        if k in mc:
            print('mc', k, mc[k] if k!='params' else f'{len(mc[k])} keys')
PY

Inspect Optuna JSON structure for #199/mode


cd /home/hoon/kis_bot && python3 - <<'PY'
import json
from pathlib import Path
p = Path('kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json')
d = json.loads(p.read_text())
print('start', d['start'], 'end', d['end'])
print('best_trial', d.get('optuna_best_trial_number'), 'best_value', d.get('optuna_best_value'))
mc = d['mode_combo']
print('mode_combo.backtest', mc.get('backtest'))
print('mode_combo.vs_best', mc.get('vs_best'))
print('mode_combo.freq sample', list((mc.get('freq') or {}).items())[:5])
# find 199 in results*
for key in ('results','results_all','results_gated','results_stable','results_learning'):
    arr = d.get(key) or []
    hit = [t for t in arr if isinstance(t, dict) and t.get('optuna_trial_number')==199]
    print(key, 'len', len(arr), 'hit199', len(hit))
    if hit:
        t=hit[0]
        print('  pnl', t.get('total_pnl'), 'trades', t.get('total_trades'), 'wr', t.get('win_rate'), 'pf', t.get('pf'))
# also scan all for max pnl
best=None
for key in ('results_all','results','results_gated'):
    for t in d.get(key) or []:
        if not isinstance(t, dict):
            continue
        if best is None or (t.get('total_pnl') or -1e18) > (best.get('total_pnl') or -1e18):
            best=t
print('max in files', best.get('optuna_trial_number') if best else None, best.get('total_pnl') if best else None, best.get('total_trades') if best else None)
PY

Find trial 199 and mode_combo stats


cd /home/hoon/kis_bot && python3 - <<'PY'
import json
from pathlib import Path
d=json.loads(Path('kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json').read_text())
t=[x for x in d['results_all'] if x.get('optuna_trial_number')==199][0]
print('199 merged keys', sorted((t.get('merged_params') or {}).keys()))
print('199 params', t['params'])
print('mode params', d['mode_combo']['params'])
# portfolio from json
for k in ('max_stocks','slot_money','total_budget'):
    print(k, d.get(k))
print('max_stocks field', d.get('max_stocks'))
mp=t.get('merged_params') or {}
print('merged portfolio', {k:mp.get(k) for k in ('max_stocks','slot_money','total_budget_krw','_orderbook_filter_enabled','backtest_tick_exit','backtest_tick_fallback_ohlc','use_whipsaw_filter')})
mmp=d['mode_combo']['params']
# mode may only have grid params
print('mode has merged?', 'merged_params' in d['mode_combo'])
PY

Extract #199 and mode params for re-eval


DB는 안 건드리고 Optuna 경로로 #199·mode만 재평가해 JSON과 맞는지 확인합니다.

cd /home/hoon/kis_bot && mkdir -p logs && TS=$(date +%Y%m%d_%H%M%S) && LOG="logs/momentum_optuna_parity_${TS}.log" && OUT="logs/momentum_optuna_parity_${TS}.json" && nohup python3 -u - <<PY >"$LOG" 2>&1 &
import json, time, traceback
from pathlib import Path

OUT = Path("$OUT")
JSON_PATH = Path("kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json")

def main():
    t0 = time.time()
    d = json.loads(JSON_PATH.read_text())
    start, end = d["start"], d["end"]
    slot = float(d["slot_money"])
    max_stocks = int(d["max_stocks"])
    budget = float(d["total_budget_krw"])
    grid_keys = list(d["grid_keys"])

    t199 = next(x for x in d["results_all"] if x.get("optuna_trial_number") == 199)
    mode_params = dict(d["mode_combo"]["params"])
    # ratchet_tiers may be in mode but not grid_keys always — keep as combo
    p199 = dict(t199["params"])

    from kis_trader.backtest.optuna_momentum import prepare_momentum_search_context
    from kis_trader.backtest.param_search_momentum import evaluate_momentum_param_combo

    print(f"prepare {start}~{end} slot={slot} max={max_stocks} budget={budget} OB=off", flush=True)
    ctx = prepare_momentum_search_context(
        start, end, "tpe",
        slot_money=slot,
        max_stocks=max_stocks,
        total_budget_krw=budget,
        orderbook_filter="off",
        market="KR",
    )
    if ctx is None:
        raise SystemExit("prepare failed")

    # Optuna 학습과 동일: 휩쏘/호가 강제 OFF (DB에 ON이어도 비교용으로 고정)
    base = dict(ctx.base_fixed)
    base["_orderbook_filter_enabled"] = False
    for k in (
        "whipsaw_filter_enabled", "use_whipsaw_filter",
        "_whipsaw_filter_enabled", "MOMENTUM_WHIPSAW_FILTER_ENABLED",
    ):
        if k in base:
            base[k] = False
    # 흔한 엔진 키
    for k in list(base.keys()):
        if "whipsaw" in k.lower() and isinstance(base[k], (bool, int)):
            base[k] = False if isinstance(base[k], bool) else 0

    print(
        f"ctx ready candles={len(ctx.codes_candles)} ticks={'yes' if ctx.ticks_by_code else 'no'} "
        f"ob_snaps={len(ctx.orderbook_by_code)} fee={ctx.fee_rate} tax={ctx.sell_tax}",
        flush=True,
    )
    print(
        f"base tick_entry={base.get('backtest_use_tick_entry')} tick_exit={base.get('backtest_use_tick_exit')} "
        f"ob={base.get('_orderbook_filter_enabled')} whip_keys={[k for k in base if 'whip' in k.lower()]}",
        flush=True,
    )

    def run_one(label, combo, recorded):
        print(f"--- eval {label} ---", flush=True)
        r = evaluate_momentum_param_combo(
            combo,
            base_fixed=base,
            grid_keys=grid_keys,
            codes_candles=ctx.codes_candles,
            min_trades=1,
            min_win_rate=0.0,
            min_pf=0.0,
            universe_by_slot=ctx.universe_by_slot,
            slot_money=ctx.slot_money,
            max_stocks=ctx.max_stocks,
            total_budget_krw=ctx.total_budget_krw,
            fee_rate=ctx.fee_rate,
            sell_tax=ctx.sell_tax,
            period_days=ctx.period_days,
            cache_holder=ctx.cache_holder,
            ticks_by_code=ctx.ticks_by_code,
            orderbook_by_code=ctx.orderbook_by_code,
            program_by_code=ctx.program_by_code,
            log_verdict_by_code=ctx.log_verdict_by_code,
            start_key=ctx.start_key,
            end_key=ctx.end_key,
            include_trades=False,
        )
        if not r:
            return {"label": label, "ok": False, "error": "evaluate returned None"}
        out = {
            "label": label,
            "ok": True,
            "reeval": {
                "total_pnl": r.get("total_pnl"),
                "total_trades": r.get("total_trades"),
                "win_rate": r.get("win_rate"),
                "pf": r.get("pf"),
                "mdd": r.get("mdd"),
            },
            "recorded": recorded,
            "delta_pnl": float(r.get("total_pnl") or 0) - float(recorded.get("total_pnl") or 0),
            "delta_trades": int(r.get("total_trades") or 0) - int(recorded.get("total_trades") or 0),
            "match": (
                int(r.get("total_pnl") or 0) == int(recorded.get("total_pnl") or 0)
                and int(r.get("total_trades") or 0) == int(recorded.get("total_trades") or 0)
            ),
        }
        print(json.dumps(out, ensure_ascii=False), flush=True)
        return out

    rows = []
    rows.append(run_one(
        "#199",
        p199,
        {
            "total_pnl": t199["total_pnl"],
            "total_trades": t199["total_trades"],
            "win_rate": t199["win_rate"],
            "pf": t199["pf"],
        },
    ))
    rows.append(run_one(
        "mode_combo",
        mode_params,
        {
            "total_pnl": d["mode_combo"]["backtest"]["total_pnl"],
            "total_trades": d["mode_combo"]["backtest"]["total_trades"],
            "win_rate": d["mode_combo"]["backtest"]["win_rate"],
            "pf": d["mode_combo"]["backtest"]["pf"],
        },
    ))

    report = {
        "source_json": str(JSON_PATH),
        "period": {"start": start, "end": end},
        "portfolio": {"slot_money": slot, "max_stocks": max_stocks, "total_budget_krw": budget},
        "orderbook": "off",
        "db_touched": False,
        "elapsed_sec": round(time.time() - t0, 1),
        "comparisons": rows,
        "note": "Optuna evaluate 경로 재실행(웹엔진=run_momentum_backtest_web_aligned). DB 미변경.",
    }
    OUT.write_text(json.dumps(report, ensure_ascii=False, indent=2))
    print("WROTE", OUT, flush=True)
    print("FINAL", json.dumps({r['label']: {'match': r.get('match'), 'delta_pnl': r.get('delta_pnl'), 'delta_trades': r.get('delta_trades')} for r in rows}, ensure_ascii=False), flush=True)

try:
    main()
except Exception:
    traceback.print_exc()
    raise
PY
echo "LOG=$LOG"
echo "OUT=$OUT"
echo "PID=$!"

Background Optuna parity re-eval no DB


ls -lt /home/hoon/kis_bot/logs/momentum_optuna_parity_* 2>/dev/null | head -10; ps -p 352593 -o pid,etime,cmd 2>/dev/null || echo 'pid gone'; ls -lt /home/hoon/.cursor/projects/home-hoon-kis-bot/terminals/*.txt 2>/dev/null | head -5; pgrep -af momentum_optuna_parity | head -5; pgrep -af 'prepare_momentum|optuna_parity' | head -10

Find parity job log and process


Write

#!/usr/bin/env python3
"""Optuna JSON(#199·mode) vs 동일 엔진 재평가 — DB 미변경.

Usage:
  python3 -u scripts/momentum_optuna_parity_reeval.py \\
    --json kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json \\
    --out logs/momentum_optuna_parity_OUT.json
"""
from __future__ import annotations

import argparse
import json
import time
import traceback
from pathlib import Path

def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument(
        "--json",
        default="kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json",
    )
    ap.add_argument("--out", required=True)
    args = ap.parse_args()

    t0 = time.time()
    json_path = Path(args.json)
    out_path = Path(args.out)
    d = json.loads(json_path.read_text())
    start, end = d["start"], d["end"]
    slot = float(d["slot_money"])
    max_stocks = int(d["max_stocks"])
    budget = float(d["total_budget_krw"])
    grid_keys = list(d["grid_keys"])

    t199 = next(x for x in d["results_all"] if x.get("optuna_trial_number") == 199)
    mode_params = dict(d["mode_combo"]["params"])
    p199 = dict(t199["params"])

    from kis_trader.backtest.optuna_momentum import prepare_momentum_search_context
    from kis_trader.backtest.param_search_momentum import evaluate_momentum_param_combo

    print(
        f"prepare {start}~{end} slot={slot} max={max_stocks} budget={budget} OB=off",
        flush=True,
    )
    ctx = prepare_momentum_search_context(
        start,
        end,
        "tpe",
        slot_money=slot,
        max_stocks=max_stocks,
        total_budget_krw=budget,
        orderbook_filter="off",
        market="KR",
    )
    if ctx is None:
        print("prepare failed", flush=True)
        return 1

    # Optuna 학습과 동일: 호가 OFF. 휩쏘 키는 있으면 OFF (DB ON이어도 비교용 고정)
    base = dict(ctx.base_fixed)
    base["_orderbook_filter_enabled"] = False
    for k in list(base.keys()):
        if "whipsaw" in k.lower() and isinstance(base[k], (bool, int)):
            base[k] = False if isinstance(base[k], bool) else 0

    print(
        f"ctx ready candles={len(ctx.codes_candles)} "
        f"ticks={'yes' if ctx.ticks_by_code else 'no'} "
        f"ob_snaps={len(ctx.orderbook_by_code)} fee={ctx.fee_rate} tax={ctx.sell_tax}",
        flush=True,
    )
    print(
        f"base tick_entry={base.get('backtest_use_tick_entry')} "
        f"tick_exit={base.get('backtest_use_tick_exit')} "
        f"ob={base.get('_orderbook_filter_enabled')} "
        f"whip_keys={[k for k in base if 'whip' in k.lower()]}",
        flush=True,
    )

    def run_one(label, combo, recorded):
        print(f"--- eval {label} ---", flush=True)
        r = evaluate_momentum_param_combo(
            combo,
            base_fixed=base,
            grid_keys=grid_keys,
            codes_candles=ctx.codes_candles,
            min_trades=1,
            min_win_rate=0.0,
            min_pf=0.0,
            universe_by_slot=ctx.universe_by_slot,
            slot_money=ctx.slot_money,
            max_stocks=ctx.max_stocks,
            total_budget_krw=ctx.total_budget_krw,
            fee_rate=ctx.fee_rate,
            sell_tax=ctx.sell_tax,
            period_days=ctx.period_days,
            cache_holder=ctx.cache_holder,
            ticks_by_code=ctx.ticks_by_code,
            orderbook_by_code=ctx.orderbook_by_code,
            program_by_code=ctx.program_by_code,
            log_verdict_by_code=ctx.log_verdict_by_code,
            start_key=ctx.start_key,
            end_key=ctx.end_key,
            include_trades=False,
        )
        if not r:
            return {"label": label, "ok": False, "error": "evaluate returned None"}
        out = {
            "label": label,
            "ok": True,
            "reeval": {
                "total_pnl": r.get("total_pnl"),
                "total_trades": r.get("total_trades"),
                "win_rate": r.get("win_rate"),
                "pf": r.get("pf"),
                "mdd": r.get("mdd"),
            },
            "recorded": recorded,
            "delta_pnl": float(r.get("total_pnl") or 0) - float(recorded.get("total_pnl") or 0),
            "delta_trades": int(r.get("total_trades") or 0)
            - int(recorded.get("total_trades") or 0),
            "match": (
                int(r.get("total_pnl") or 0) == int(recorded.get("total_pnl") or 0)
                and int(r.get("total_trades") or 0)
                == int(recorded.get("total_trades") or 0)
            ),
        }
        print(json.dumps(out, ensure_ascii=False), flush=True)
        return out

    rows = [
        run_one(
            "#199",
            p199,
            {
                "total_pnl": t199["total_pnl"],
                "total_trades": t199["total_trades"],
                "win_rate": t199["win_rate"],
                "pf": t199["pf"],
            },
        ),
        run_one(
            "mode_combo",
            mode_params,
            {
                "total_pnl": d["mode_combo"]["backtest"]["total_pnl"],
                "total_trades": d["mode_combo"]["backtest"]["total_trades"],
                "win_rate": d["mode_combo"]["backtest"]["win_rate"],
                "pf": d["mode_combo"]["backtest"]["pf"],
            },
        ),
    ]

    report = {
        "source_json": str(json_path),
        "period": {"start": start, "end": end},
        "portfolio": {
            "slot_money": slot,
            "max_stocks": max_stocks,
            "total_budget_krw": budget,
        },
        "orderbook": "off",
        "db_touched": False,
        "elapsed_sec": round(time.time() - t0, 1),
        "comparisons": rows,
        "note": (
            "Optuna evaluate 경로 재실행"
            "(웹엔진=run_momentum_backtest_web_aligned). DB 미변경."
        ),
        "web_bt_note": (
            "지금 웹폼(호가ON+휩쏘ON)과 숫자 비교가 아님. "
            "정합=JSON기록 vs 동일조건 재시뮬."
        ),
    }
    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_text(json.dumps(report, ensure_ascii=False, indent=2))
    print("WROTE", out_path, flush=True)
    print(
        "FINAL",
        json.dumps(
            {
                r["label"]: {
                    "match": r.get("match"),
                    "delta_pnl": r.get("delta_pnl"),
                    "delta_trades": r.get("delta_trades"),
                    "reeval": r.get("reeval"),
                }
                for r in rows
            },
            ensure_ascii=False,
        ),
        flush=True,
    )
    return 0 if all(r.get("match") for r in rows if r.get("ok")) else 2

if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except Exception:
        traceback.print_exc()
        raise SystemExit(1)


Shell

cd /home/hoon/kis_bot && pgrep -af 'python3.*(momentum_optuna|prepare_momentum|param_search)' | head -20; wc -l logs/momentum_optuna_parity_20260822_010445.log; tail -20 logs/momentum_optuna_parity_20260822_010445.log

Check if first parity job still running


ps --ppid 352593 -o pid,etime,cmd 2>/dev/null; pgrep -P 352593 -a; ls -la /proc/352593/fd 2>/dev/null | head -5; tr '\0' ' ' < /proc/352593/cmdline 2>/dev/null | head -c 200; echo; pstree -p 352593 2>/dev/null | head -20; ls -lt /home/hoon/kis_bot/logs/momentum_optuna_parity_*

Inspect stuck job process tree


cd /home/hoon/kis_bot && kill 352601 352593 2>/dev/null; sleep 1; TS=$(date +%Y%m%d_%H%M%S); LOG="logs/momentum_optuna_parity_${TS}.log"; OUT="logs/momentum_optuna_parity_${TS}.json"; nohup python3 -u scripts/momentum_optuna_parity_reeval.py --out "$OUT" >"$LOG" 2>&1 & echo "PID=$! LOG=$LOG OUT=$OUT"; sleep 2; head -15 "$LOG"

Restart parity re-eval via script file


cd /home/hoon/kis_bot && TS=$(date +%Y%m%d_%H%M%S); LOG="logs/momentum_optuna_parity_${TS}.log"; OUT="logs/momentum_optuna_parity_${TS}.json"; PYTHONPATH=/home/hoon/kis_bot nohup python3 -u scripts/momentum_optuna_parity_reeval.py --out "$OUT" >"$LOG" 2>&1 & echo "PID=$! LOG=$LOG OUT=$OUT"; sleep 3; head -20 "$LOG"

Run parity script with PYTHONPATH


cd /home/hoon/kis_bot && for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do if grep -q 'FINAL\|Traceback\|prepare failed' logs/momentum_optuna_parity_20260822_010541.log 2>/dev/null; then echo DONE; tail -40 logs/momentum_optuna_parity_20260822_010541.log; break; fi; echo "wait $i $(date +%H:%M:%S) lines=$(wc -l < logs/momentum_optuna_parity_20260822_010541.log)"; sleep 15; done; pgrep -af 'momentum_optuna_parity_reeval' | head -3; ls -la logs/momentum_optuna_parity_20260822_010541.*

Poll parity log until FINAL


tail -30 /home/hoon/kis_bot/logs/momentum_optuna_parity_20260822_010541.log; pgrep -af momentum_optuna_parity_reeval | head -3; ls -la /home/hoon/kis_bot/logs/momentum_optuna_parity_20260822_010541.json 2>/dev/null || echo 'no json yet'

Check parity job progress


재평가가 JSON과 안 맞습니다. DB는 그대로 두고, 당시 `merged_params`와 지금 `base_fixed` 차이를 짚겠습니다.

cd /home/hoon/kis_bot && PYTHONPATH=/home/hoon/kis_bot python3 - <<'PY'
import json
from pathlib import Path
from kis_trader.backtest.param_search_momentum import _mom_fixed_defaults
from kis_trader.engine.momentum_tick_replay import (
    momentum_backtest_use_tick_entry as te,
    momentum_backtest_use_tick_exit as tx,
)

d=json.loads(Path('kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json').read_text())
t=next(x for x in d['results_all'] if x.get('optuna_trial_number')==199)
mp=dict(t['merged_params'])
base=_mom_fixed_defaults()
base['backtest_use_tick_entry']=te(None)
base['backtest_use_tick_exit']=tx(None)
base['backtest_tick_fallback_ohlc']=False
base['_orderbook_filter_enabled']=False

# compare all keys in merged
diffs=[]
only_mp=[]
only_base=[]
for k in sorted(set(mp)|set(base)):
    if k in ('params',):
        continue
    if k not in mp:
        only_base.append(k); continue
    if k not in base:
        only_mp.append(k); continue
    a,b=mp[k],base[k]
    # float tol
    try:
        if isinstance(a,(int,float)) and isinstance(b,(int,float)):
            if abs(float(a)-float(b))>1e-9:
                diffs.append((k,a,b))
            continue
    except Exception:
        pass
    if a!=b:
        diffs.append((k,a,b))

print('diffs count', len(diffs))
for row in diffs[:40]:
    print('DIFF', row)
print('only_in_merged', only_mp)
print('only_in_base sample', only_base[:30])
# portfolio
print('merged port', {k:mp.get(k) for k in ('slot_money','max_stocks','total_budget_krw','fee_rate','sell_tax','scan_interval_min','_universe_history_source')})
print('tick', mp.get('backtest_use_tick_entry'), mp.get('backtest_use_tick_exit'), mp.get('backtest_tick_fallback_ohlc'))
PY

Diff #199 merged_params vs current fixed defaults


cd /home/hoon/kis_bot && PYTHONPATH=/home/hoon/kis_bot python3 - <<'PY'
import json
from pathlib import Path
d=json.loads(Path('kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json').read_text())
t=next(x for x in d['results_all'] if x.get('optuna_trial_number')==199)
mp=t['merged_params']
# fixed keys not in params
params=set(t['params'])
fixed={k:v for k,v in mp.items() if k not in params}
print('fixed rsi', fixed.get('mom_rsi_min'), fixed.get('mom_rsi_max'))
print('fee', fixed.get('fee_rate'), fixed.get('sell_tax'))
print('univ', fixed.get('_universe_history_source'), fixed.get('scan_interval_min'))
print('skip_hts', fixed.get('skip_hts_scan_dupes'))
print('defense', {k:fixed.get(k) for k in fixed if 'defense' in k or 'rsi' in k or 'high_chase' in k or 'daily_range' in k})
# json meta for history?
print('json keys with univ', [k for k in d if 'univ' in k.lower() or 'history' in k.lower()])
PY

Inspect #199 fixed non-grid merged keys


원인: Optuna 당시 고정값(RSI 51/77 등)과 지금 DB 기본값이 다릅니다. 당시 `merged_params`로 고정해 재평가합니다.

cd /home/hoon/kis_bot && TS=$(date +%Y%m%d_%H%M%S) && LOG="logs/momentum_optuna_parity_merged_${TS}.log" && OUT="logs/momentum_optuna_parity_merged_${TS}.json" && PYTHONPATH=/home/hoon/kis_bot nohup python3 -u - <<'PY' >"$LOG" 2>&1 &
import json, time, traceback
from pathlib import Path
from copy import deepcopy

OUT = Path("""$OUT""")
JSON_PATH = Path("kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json")

def main():
    t0 = time.time()
    d = json.loads(JSON_PATH.read_text())
    start, end = d["start"], d["end"]
    slot = float(d["slot_money"])
    max_stocks = int(d["max_stocks"])
    budget = float(d["total_budget_krw"])
    grid_keys = list(d["grid_keys"])
    t199 = next(x for x in d["results_all"] if x.get("optuna_trial_number") == 199)
    mp = dict(t199["merged_params"])
    p199 = dict(t199["params"])
    mode_params = dict(d["mode_combo"]["params"])
    # Optuna 당시 고정축 스냅샷 (지금 DB와 다를 수 있음)
    fixed_snap = {k: v for k, v in mp.items() if k not in p199}
    fixed_snap["_orderbook_filter_enabled"] = False
    for k in list(fixed_snap.keys()):
        if "whipsaw" in k.lower() and isinstance(fixed_snap[k], (bool, int)):
            fixed_snap[k] = False if isinstance(fixed_snap[k], bool) else 0

    from kis_trader.backtest.optuna_momentum import prepare_momentum_search_context
    from kis_trader.backtest.param_search_momentum import evaluate_momentum_param_combo

    print(f"prepare {start}~{end} OB=off (merged fixed snap)", flush=True)
    print(f"fixed rsi={fixed_snap.get('mom_rsi_min')}/{fixed_snap.get('mom_rsi_max')} fee_ui={fixed_snap.get('fee_rate')}", flush=True)
    ctx = prepare_momentum_search_context(
        start, end, "tpe",
        slot_money=slot, max_stocks=max_stocks, total_budget_krw=budget,
        orderbook_filter="off", market="KR",
        history_source="kiwoom",
    )
    if ctx is None:
        raise SystemExit("prepare failed")

    base = dict(ctx.base_fixed)
    base.update(fixed_snap)
    base["_orderbook_filter_enabled"] = False

    def run_one(label, combo, recorded):
        print(f"--- eval {label} ---", flush=True)
        r = evaluate_momentum_param_combo(
            combo, base_fixed=base, grid_keys=grid_keys,
            codes_candles=ctx.codes_candles, min_trades=1, min_win_rate=0.0, min_pf=0.0,
            universe_by_slot=ctx.universe_by_slot,
            slot_money=ctx.slot_money, max_stocks=ctx.max_stocks,
            total_budget_krw=ctx.total_budget_krw,
            fee_rate=ctx.fee_rate, sell_tax=ctx.sell_tax, period_days=ctx.period_days,
            cache_holder=ctx.cache_holder, ticks_by_code=ctx.ticks_by_code,
            orderbook_by_code=ctx.orderbook_by_code, program_by_code=ctx.program_by_code,
            log_verdict_by_code=ctx.log_verdict_by_code,
            start_key=ctx.start_key, end_key=ctx.end_key, include_trades=False,
        )
        if not r:
            return {"label": label, "ok": False}
        out = {
            "label": label, "ok": True,
            "reeval": {"total_pnl": r["total_pnl"], "total_trades": r["total_trades"], "win_rate": r["win_rate"], "pf": r["pf"]},
            "recorded": recorded,
            "delta_pnl": float(r["total_pnl"]) - float(recorded["total_pnl"]),
            "delta_trades": int(r["total_trades"]) - int(recorded["total_trades"]),
            "match": int(r["total_pnl"]) == int(recorded["total_pnl"]) and int(r["total_trades"]) == int(recorded["total_trades"]),
        }
        print(json.dumps(out, ensure_ascii=False), flush=True)
        return out

    rows = [
        run_one("#199", p199, {"total_pnl": t199["total_pnl"], "total_trades": t199["total_trades"], "win_rate": t199["win_rate"], "pf": t199["pf"]}),
        run_one("mode_combo", mode_params, {"total_pnl": d["mode_combo"]["backtest"]["total_pnl"], "total_trades": d["mode_combo"]["backtest"]["total_trades"], "win_rate": d["mode_combo"]["backtest"]["win_rate"], "pf": d["mode_combo"]["backtest"]["pf"]}),
    ]
    report = {
        "method": "merged_fixed_snapshot + combo",
        "db_touched": False,
        "elapsed_sec": round(time.time() - t0, 1),
        "fixed_rsi": [fixed_snap.get("mom_rsi_min"), fixed_snap.get("mom_rsi_max")],
        "comparisons": rows,
    }
    OUT.write_text(json.dumps(report, ensure_ascii=False, indent=2))
    print("WROTE", OUT, flush=True)
    print("FINAL", json.dumps({r["label"]: {"match": r.get("match"), "delta_pnl": r.get("delta_pnl"), "delta_trades": r.get("delta_trades"), "reeval": r.get("reeval")} for r in rows}, ensure_ascii=False), flush=True)

try:
    main()
except Exception:
    traceback.print_exc()
    raise
PY
echo "PID=$! LOG=$LOG OUT=$OUT"

Re-eval with Optuna-time merged fixed snap


Write

#!/usr/bin/env python3
"""Optuna JSON 재평가 — 당시 merged_params 고정축 스냅샷 사용 (DB 미변경)."""
from __future__ import annotations

import argparse
import json
import time
import traceback
from pathlib import Path

def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument(
        "--json",
        default="kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json",
    )
    ap.add_argument("--out", required=True)
    args = ap.parse_args()

    t0 = time.time()
    json_path = Path(args.json)
    out_path = Path(args.out)
    d = json.loads(json_path.read_text())
    start, end = d["start"], d["end"]
    slot = float(d["slot_money"])
    max_stocks = int(d["max_stocks"])
    budget = float(d["total_budget_krw"])
    grid_keys = list(d["grid_keys"])

    t199 = next(x for x in d["results_all"] if x.get("optuna_trial_number") == 199)
    mp = dict(t199["merged_params"])
    p199 = dict(t199["params"])
    mode_params = dict(d["mode_combo"]["params"])

    # Optuna 당시 고정축 (지금 DB 기본값과 다를 수 있음 — RSI 등)
    fixed_snap = {k: v for k, v in mp.items() if k not in p199}
    fixed_snap["_orderbook_filter_enabled"] = False
    for k in list(fixed_snap.keys()):
        if "whipsaw" in k.lower() and isinstance(fixed_snap[k], (bool, int)):
            fixed_snap[k] = False if isinstance(fixed_snap[k], bool) else 0

    from kis_trader.backtest.optuna_momentum import prepare_momentum_search_context
    from kis_trader.backtest.param_search_momentum import evaluate_momentum_param_combo

    print(
        f"prepare {start}~{end} OB=off merged-fixed "
        f"rsi={fixed_snap.get('mom_rsi_min')}/{fixed_snap.get('mom_rsi_max')}",
        flush=True,
    )
    ctx = prepare_momentum_search_context(
        start,
        end,
        "tpe",
        slot_money=slot,
        max_stocks=max_stocks,
        total_budget_krw=budget,
        orderbook_filter="off",
        market="KR",
        history_source="kiwoom",
    )
    if ctx is None:
        print("prepare failed", flush=True)
        return 1

    base = dict(ctx.base_fixed)
    base.update(fixed_snap)
    base["_orderbook_filter_enabled"] = False

    def run_one(label, combo, recorded):
        print(f"--- eval {label} ---", flush=True)
        r = evaluate_momentum_param_combo(
            combo,
            base_fixed=base,
            grid_keys=grid_keys,
            codes_candles=ctx.codes_candles,
            min_trades=1,
            min_win_rate=0.0,
            min_pf=0.0,
            universe_by_slot=ctx.universe_by_slot,
            slot_money=ctx.slot_money,
            max_stocks=ctx.max_stocks,
            total_budget_krw=ctx.total_budget_krw,
            fee_rate=ctx.fee_rate,
            sell_tax=ctx.sell_tax,
            period_days=ctx.period_days,
            cache_holder=ctx.cache_holder,
            ticks_by_code=ctx.ticks_by_code,
            orderbook_by_code=ctx.orderbook_by_code,
            program_by_code=ctx.program_by_code,
            log_verdict_by_code=ctx.log_verdict_by_code,
            start_key=ctx.start_key,
            end_key=ctx.end_key,
            include_trades=False,
        )
        if not r:
            return {"label": label, "ok": False, "error": "None"}
        out = {
            "label": label,
            "ok": True,
            "reeval": {
                "total_pnl": r.get("total_pnl"),
                "total_trades": r.get("total_trades"),
                "win_rate": r.get("win_rate"),
                "pf": r.get("pf"),
            },
            "recorded": recorded,
            "delta_pnl": float(r.get("total_pnl") or 0) - float(recorded.get("total_pnl") or 0),
            "delta_trades": int(r.get("total_trades") or 0)
            - int(recorded.get("total_trades") or 0),
            "match": (
                int(r.get("total_pnl") or 0) == int(recorded.get("total_pnl") or 0)
                and int(r.get("total_trades") or 0)
                == int(recorded.get("total_trades") or 0)
            ),
        }
        print(json.dumps(out, ensure_ascii=False), flush=True)
        return out

    rows = [
        run_one(
            "#199",
            p199,
            {
                "total_pnl": t199["total_pnl"],
                "total_trades": t199["total_trades"],
                "win_rate": t199["win_rate"],
                "pf": t199["pf"],
            },
        ),
        run_one(
            "mode_combo",
            mode_params,
            {
                "total_pnl": d["mode_combo"]["backtest"]["total_pnl"],
                "total_trades": d["mode_combo"]["backtest"]["total_trades"],
                "win_rate": d["mode_combo"]["backtest"]["win_rate"],
                "pf": d["mode_combo"]["backtest"]["pf"],
            },
        ),
    ]
    report = {
        "method": "merged_fixed_snapshot + combo",
        "source_json": str(json_path),
        "db_touched": False,
        "elapsed_sec": round(time.time() - t0, 1),
        "fixed_rsi": [fixed_snap.get("mom_rsi_min"), fixed_snap.get("mom_rsi_max")],
        "comparisons": rows,
    }
    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_text(json.dumps(report, ensure_ascii=False, indent=2))
    print("WROTE", out_path, flush=True)
    print(
        "FINAL",
        json.dumps(
            {
                r["label"]: {
                    "match": r.get("match"),
                    "delta_pnl": r.get("delta_pnl"),
                    "delta_trades": r.get("delta_trades"),
                    "reeval": r.get("reeval"),
                }
                for r in rows
            },
            ensure_ascii=False,
        ),
        flush=True,
    )
    return 0 if all(r.get("match") for r in rows if r.get("ok")) else 2

if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except Exception:
        traceback.print_exc()
        raise SystemExit(1)


Shell

cd /home/hoon/kis_bot && pkill -f 'python3 -u -' 2>/dev/null; pkill -f momentum_optuna_parity 2>/dev/null; sleep 1; TS=$(date +%Y%m%d_%H%M%S); LOG=logs/momentum_optuna_parity_merged_${TS}.log; OUT=logs/momentum_optuna_parity_merged_${TS}.json; PYTHONPATH=/home/hoon/kis_bot nohup python3 -u scripts/momentum_optuna_parity_merged_snap.py --out "$OUT" >"$LOG" 2>&1 & echo PID=$!; echo LOG=$LOG; echo OUT=$OUT; sleep 2; head -8 "$LOG"

Start merged-snapshot parity re-eval


cd /home/hoon/kis_bot; TS=$(date +%Y%m%d_%H%M%S); LOG="logs/momentum_optuna_parity_merged_${TS}.log"; OUT="logs/momentum_optuna_parity_merged_${TS}.json"; export PYTHONPATH=/home/hoon/kis_bot; nohup python3 -u scripts/momentum_optuna_parity_merged_snap.py --out "$OUT" >"$LOG" 2>&1 & echo "PID=$! LOG=$LOG OUT=$OUT"; sleep 3; wc -l "$LOG"; head -12 "$LOG"; pgrep -af momentum_optuna_parity_merged_snap | head -3

Launch merged snap parity job cleanly


cd /home/hoon/kis_bot; LOG=logs/momentum_optuna_parity_merged_20260822_011004.log; for i in $(seq 1 24); do if grep -q 'FINAL\|Traceback' "$LOG" 2>/dev/null; then echo DONE_AT_$i; grep -E 'FINAL|WROTE|"label"|match' "$LOG" | tail -20; break; fi; echo "wait $i $(date +%H:%M:%S) $(wc -l < $LOG)l"; sleep 10; done; test -f logs/momentum_optuna_parity_merged_20260822_011004.json && python3 -c "import json;print(json.load(open('logs/momentum_optuna_parity_merged_20260822_011004.json')))"

Wait for merged snap parity FINAL


tail -15 /home/hoon/kis_bot/logs/momentum_optuna_parity_merged_20260822_011004.log; ls -la /home/hoon/kis_bot/logs/momentum_optuna_parity_merged_20260822_011004.json 2>/dev/null; pgrep -af momentum_optuna_parity_merged_snap | head -2

Check merged parity status now


sleep 60; grep -E 'FINAL|WROTE|"label"|Traceback|--- eval' /home/hoon/kis_bot/logs/momentum_optuna_parity_merged_20260822_011004.log | tail -30; test -f /home/hoon/kis_bot/logs/momentum_optuna_parity_merged_20260822_011004.json && cat /home/hoon/kis_bot/logs/momentum_optuna_parity_merged_20260822_011004.json; pgrep -c -f momentum_optuna_parity_merged_snap || echo done_proc

Wait 60s then read FINAL result


cd /home/hoon/kis_bot && PYTHONPATH=/home/hoon/kis_bot python3 - <<'PY'
# 웹 최근 BT(호가ON) vs Optuna JSON — 숫자만 표로
import json
from pathlib import Path
opt=json.loads(Path('kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json').read_text())
t=next(x for x in opt['results_all'] if x.get('optuna_trial_number')==199)
# recent web bt
cands=sorted(Path('kis_trader/backtest/results').glob('momentum_bt_*.json'), key=lambda p:p.stat().st_mtime, reverse=True)[:5]
print('recent web bt:')
for p in cands:
    try:
        d=json.loads(p.read_text())
    except Exception as e:
        print(p.name, e); continue
    s=d.get('summary') or d.get('stats') or {}
    print(p.name, 'pnl', s.get('total_pnl') or d.get('total_pnl'), 'trades', s.get('total_trades') or d.get('total_trades'), 'keys', list(d.keys())[:8])
print()
print('=== 정합표 (DB미변경 재시뮬) ===')
print(f"{'케이스':<22} {'PnL':>10} {'건수':>6} {'WR':>7} {'PF':>6}")
print(f"{'Optuna #199 기록':<22} {t['total_pnl']:>10.0f} {t['total_trades']:>6} {t['win_rate']:>7.2f} {t['pf']:>6.2f}")
print(f"{'재시뮬 #199 OB=off':<22} {15150:>10} {48:>6} {60.42:>7.2f} {1.06:>6.2f}")
mc=opt['mode_combo']['backtest']
print(f"{'Optuna mode 기록':<22} {mc['total_pnl']:>10.0f} {mc['total_trades']:>6} {mc['win_rate']:>7.2f} {mc['pf']:>6.2f}")
print(f"{'재시뮬 mode OB=off':<22} {-6248:>10} {53:>6} {58.49:>7.2f} {0.97:>6.2f}")
PY

Build comparison table with recent web BT


ls -lt /home/hoon/kis_bot/logs/optuna*momentum*20260821* /home/hoon/kis_bot/logs/*momentum*tpe*20260821* 2>/dev/null | head -20; rg -l 'optuna_study_name.*momentum_tpe_20260818' /home/hoon/kis_bot/logs 2>/dev/null | head -10; rg -n 'ws_ticks|40882|✅ 데이터 로드' /home/hoon/kis_bot/logs/optuna_web_jobs/opt_20260821_214129_seq.json 2>/dev/null | head -20

Find original Optuna momentum run log


Grep

ws_ticks|데이터 로드|봉 pick|쓰레기|REST 웜업|호가필터|포트폴리오|틱 전무|40882|trial_number.: 199

Shell

cd /home/hoon/kis_bot && PYTHONPATH=/home/hoon/kis_bot python3 - <<'PY'
# 웹BT 004905 params vs mode / #199 — OB/whip만 확인
import json
from pathlib import Path
w=json.loads(Path('kis_trader/backtest/results/momentum_bt_20260818_20260821_20260822_004905.json').read_text())
p=w.get('params') or {}
keys=['_orderbook_filter_enabled','ob_filter','whipsaw','chase_lookback','sl_pct','mom_vol_mult','use_ema']
print('web summary', w.get('summary'))
for k,v in sorted(p.items()):
    if any(x in k.lower() for x in ('orderbook','whip','chase','sl_pct','vol_mult','ema','rsi','ob_')):
        print(k,v)
PY

Inspect web BT params for OB/whip


Grep

n_jobs|n_trials|Trial 199|total_pnl.: 40882|Best trial

Shell

cd /home/hoon/kis_bot; TS=$(date +%Y%m%d_%H%M%S); LOG="logs/momentum_optuna_parity_noshared_${TS}.log"; OUT="logs/momentum_optuna_parity_noshared_${TS}.json"; export PYTHONPATH=/home/hoon/kis_bot; export OPTUNA_PARAM_SEARCH_SHARED_TICKS=0; nohup python3 -u scripts/momentum_optuna_parity_merged_snap.py --out "$OUT" >"$LOG" 2>&1 & echo "PID=$! LOG=$LOG OUT=$OUT"

Re-eval with shared ticks disabled


sleep 90; grep -E 'FINAL|공유메모리|SHARED|WROTE|Traceback' /home/hoon/kis_bot/logs/momentum_optuna_parity_noshared_20260822_011401.log | tail -20; test -f /home/hoon/kis_bot/logs/momentum_optuna_parity_noshared_20260822_011401.json && cat /home/hoon/kis_bot/logs/momentum_optuna_parity_noshared_20260822_011401.json

Await noshared parity result


# noshared progress
wc -l /home/hoon/kis_bot/logs/momentum_optuna_parity_noshared_20260822_011401.log; tail -5 /home/hoon/kis_bot/logs/momentum_optuna_parity_noshared_20260822_011401.log; pgrep -c -f momentum_optuna_parity_merged_snap || echo done

Check noshared job progress


cd /home/hoon/kis_bot && git log --since='2026-08-21 21:00' --until='2026-08-22 02:00' --oneline -- kis_trader/backtest/momentum_backtest_common.py kis_trader/engine/momentum_engine.py kis_trader/backtest/optuna_momentum.py kis_trader/backtest/param_search_momentum.py kis_trader/backtest/shared_ticks.py 2>/dev/null | head -30; git log -5 --oneline -- kis_trader/backtest/momentum_backtest_common.py 2>/dev/null | head -10

Git commits after Optuna run affecting engine


cd /home/hoon/kis_bot && git log --format='%h %ci %s' -15 -- kis_trader/backtest/momentum_backtest_common.py kis_trader/backtest/bt_candle_source.py kis_trader/backtest/shared_ticks.py kis_trader/engine/momentum_engine.py 2>/dev/null; echo '---'; git show -s --format='%h %ci %s' 3eaa3b6 0780b2c 2c7ad86 2>/dev/null; echo '--- files in 3eaa3b6'; git show --stat --oneline 3eaa3b6 2>/dev/null | head -40

When were engine fixes relative to Optuna


cd /home/hoon/kis_bot && git status -sb | head -40; git diff --stat HEAD -- kis_trader/backtest/momentum_backtest_common.py kis_trader/engine/momentum_engine.py kis_trader/backtest/shared_ticks.py kis_trader/backtest/optuna_momentum.py 2>/dev/null | head -20; ls -la kis_trader/backtest/momentum_backtest_common.py; stat -c '%y' kis_trader/backtest/momentum_backtest_common.py kis_trader/engine/momentum_engine.py 2>/dev/null

Check uncommitted engine changes since Optuna


cd /home/hoon/kis_bot && git diff HEAD -- kis_trader/engine/momentum_engine.py | head -80; grep -E 'FINAL|WROTE|공유' /home/hoon/kis_bot/logs/momentum_optuna_parity_noshared_20260822_011401.log | tail -10; wc -l /home/hoon/kis_bot/logs/momentum_optuna_parity_noshared_20260822_011401.log; pgrep -af 'parity_merged|parity_reeval' | head -5

Diff momentum_engine; check noshared done


cd /home/hoon/kis_bot && PYTHONPATH=/home/hoon/kis_bot python3 - <<'PY'
import json
from pathlib import Path
d=json.loads(Path('kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json').read_text())
t=next(x for x in d['results_all'] if x.get('optuna_trial_number')==199)
mp=t['merged_params']
for k in sorted(mp):
    if 'pattern' in k or 'trigger' in k or 'defense' in k or 'skip' in k or 'bull' in k or 'confirm' in k:
        print(k, mp[k])
print('--- all keys count', len(mp))
# check mdd in user attrs vs score
print('recorded mdd', t.get('mdd'), 'score', t.get('score'), 'pnl/mdd', t['total_pnl']/max(t.get('mdd') or 1, 10000))
PY

Inspect #199 defense/pattern flags and score


cd /home/hoon/kis_bot && PYTHONPATH=/home/hoon/kis_bot python3 - <<'PY'
import json
from pathlib import Path
d=json.loads(Path('kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json').read_text())
# find trial 0 and a few early
by={t.get('optuna_trial_number'): t for t in d['results_all'] if isinstance(t,dict)}
for n in [0,1,2,10,50,100,199]:
    t=by.get(n)
    if not t:
        print(n, 'missing in results_all')
        continue
    print(n, 'pnl', t['total_pnl'], 'tr', t['total_trades'], 'mdd', t.get('mdd'), 'wr', t['win_rate'])
# also check study storage for trial 0
print('n results_all', len(d['results_all']))
print('numbers', sorted(by.keys())[:15], '...', sorted(by.keys())[-5:])
PY

List early Optuna trial PnLs from JSON


Grep

Trial 0 finished|Trial 2 finished|gates_ok|FAIL

Shell

cd /home/hoon/kis_bot; TS=$(date +%Y%m%d_%H%M%S); LOG="logs/momentum_optuna_parity_t0_${TS}.log"; OUT="logs/momentum_optuna_parity_t0_${TS}.json"; export PYTHONPATH=/home/hoon/kis_bot; export OPTUNA_PARAM_SEARCH_SHARED_TICKS=0; cat > /tmp/mom_parity_t0.py <<'EOF'
import json, time
from pathlib import Path
OUT=Path("logs/momentum_optuna_parity_t0_OUT.json")
# will set via argv
import sys
OUT=Path(sys.argv[1])
d=json.loads(Path("kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json").read_text())
t0=next(x for x in d["results_all"] if x.get("optuna_trial_number")==0)
t199=next(x for x in d["results_all"] if x.get("optuna_trial_number")==199)
grid_keys=list(d["grid_keys"])
from kis_trader.backtest.optuna_momentum import prepare_momentum_search_context
from kis_trader.backtest.param_search_momentum import evaluate_momentum_param_combo
ctx=prepare_momentum_search_context(d["start"], d["end"], "tpe", slot_money=float(d["slot_money"]), max_stocks=int(d["max_stocks"]), total_budget_krw=float(d["total_budget_krw"]), orderbook_filter="off", market="KR", history_source="kiwoom")
mp=dict(t199["merged_params"])  # fixed snap from any trial's non-grid — use t0 merged for t0
rows=[]
for label, t in [("#0", t0), ("#199", t199)]:
    params=dict(t["params"])
    fixed={k:v for k,v in dict(t["merged_params"]).items() if k not in params}
    fixed["_orderbook_filter_enabled"]=False
    base=dict(ctx.base_fixed); base.update(fixed)
    r=evaluate_momentum_param_combo(params, base_fixed=base, grid_keys=grid_keys, codes_candles=ctx.codes_candles, min_trades=1, min_win_rate=0.0, min_pf=0.0, universe_by_slot=ctx.universe_by_slot, slot_money=ctx.slot_money, max_stocks=ctx.max_stocks, total_budget_krw=ctx.total_budget_krw, fee_rate=ctx.fee_rate, sell_tax=ctx.sell_tax, period_days=ctx.period_days, cache_holder=ctx.cache_holder, ticks_by_code=ctx.ticks_by_code, orderbook_by_code=ctx.orderbook_by_code, program_by_code=ctx.program_by_code, log_verdict_by_code=ctx.log_verdict_by_code, start_key=ctx.start_key, end_key=ctx.end_key)
    rec={"total_pnl":t["total_pnl"],"total_trades":t["total_trades"],"mdd":t.get("mdd")}
    out={"label":label,"reeval":{"total_pnl":r["total_pnl"],"total_trades":r["total_trades"],"mdd":r.get("mdd")},"recorded":rec,
         "delta_pnl":r["total_pnl"]-rec["total_pnl"],"delta_trades":r["total_trades"]-rec["total_trades"],
         "match":r["total_pnl"]==int(rec["total_pnl"]) and r["total_trades"]==int(rec["total_trades"])}
    print(json.dumps(out,ensure_ascii=False), flush=True)
    rows.append(out)
OUT.write_text(json.dumps({"comparisons":rows,"db_touched":False},ensure_ascii=False,indent=2))
print("FINAL", json.dumps({r["label"]:r for r in rows},ensure_ascii=False), flush=True)
EOF
# fix OUT path in file - pass as argv
nohup python3 -u /tmp/mom_parity_t0.py "$OUT" >"$LOG" 2>&1 & echo PID=$! LOG=$LOG OUT=$OUT

Re-eval trial 0 and 199 for systematic delta


for i in 1 2 3 4 5 6 7 8 9 10 11 12; do if grep -q FINAL /home/hoon/kis_bot/logs/momentum_optuna_parity_t0_20260822_011628.log 2>/dev/null; then grep FINAL /home/hoon/kis_bot/logs/momentum_optuna_parity_t0_20260822_011628.log; cat /home/hoon/kis_bot/logs/momentum_optuna_parity_t0_20260822_011628.json; exit 0; fi; sleep 15; done; tail -5 /home/hoon/kis_bot/logs/momentum_optuna_parity_t0_20260822_011628.log; echo STILL_RUNNING

Poll trial0 parity until FINAL


DB는 **안 건드렸습니다.** 같은 기간·포트·호가 OFF로 Optuna JSON만 다시 돌린 결과입니다.

### 정합 비교 (호가 OFF · DB 미변경)

| 케이스 | Optuna 기록 | 지금 재시뮬 | Δ |
|--------|-------------|------------|---|
| **#199** | +40,882 / 52건 | +15,150 / 48건 | **−25,732 / −4건** |
| **mode** | +14,850 / 57건 | −6,248 / 53건 | **−21,098 / −4건** |
| 참고 trial#0 | −142,112 / 106건 | −133,144 / 96건 | 역시 불일치 |

틱·봉 로드 지문(건수·pick·웜업)은 어젯밤 Optuna 로그와 **동일**합니다.
공유메모리 OFF로 다시 돌려도 #199 숫자는 같았습니다.

### 그래서

1. **웹(호가ON+휩쏘, +43,046/28건)** 과 Optuna #199를 맞추려던 비교는 원래 조건이 다름 → 이번 표에 안 넣음.
2. 조건을 맞춰도 **Optuna JSON 숫자 자체가 지금 엔진으로 재현이 안 됨.**
   “웹이 틀렸다” 전에, **기록값 ↔ 동일 파이프 재시뮬** 부터 어긋나 있습니다.
3. 원인 후보는 데이터 바뀜이 아니라 **study 당시 결과 기록/상태** 쪽(재현 불능). 더 파려면 trial 체결 리스트 diff가 다음 단계입니다.

로그/결과:
`logs/momentum_optuna_parity_merged_20260822_011004.json`
`logs/momentum_optuna_parity_t0_20260822_011628.json`


<timestamp>Saturday, Aug 22, 2026, 1:19 AM (UTC+9)</timestamp>

<user_query>Briefly inform the user about the task result and perform any follow-up actions (if needed). If there's no follow-ups needed, don't explicitly say that.</user_query>


백그라운드 폴링도 모두 끝났고, 결과는 앞서 드린 표와 같습니다. DB 미변경·호가 OFF 재시뮬 기준 **#199·mode·trial#0 모두 Optuna JSON과 불일치**로 확정입니다.