feat(옵투나·웹): 후처리 재탐색·ob_modes·적용감사·수집통계

- Optuna web jobs/TPE/apply snapshot·틱로더 정합, jobs limit·감사로그
- 백테 UI 호가모드·후보 적용 흐름, feed_collect_stats API/탭
- 가설검증·교차검증 룰, 4전략 스모크·OB slot41 진단 스크립트

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Your Name
2026-08-27 15:23:44 +09:00
parent 5e44b86f8b
commit 8fbba264ba
30 changed files with 2973 additions and 186 deletions

View File

@@ -0,0 +1,146 @@
#!/usr/bin/env python3
"""Apply trial#120 params → 1일 모멘텀 백테 (호가 ON). DB 미변경."""
from __future__ import annotations
import json
import sys
import time
from pathlib import Path
_ROOT = Path(__file__).resolve().parents[1]
if str(_ROOT) not in sys.path:
sys.path.insert(0, str(_ROOT))
def main() -> int:
t0 = time.time()
start = end = "2026-08-25"
out_json = _ROOT / "logs" / "mom_bt_20260825_trial120_ob_on.json"
print(f"start prepare {start} OB=on", flush=True)
from database import TradeDB
from kis_trader.backtest.optuna_momentum import prepare_momentum_search_context
from kis_trader.backtest.param_search_momentum import evaluate_momentum_param_combo
db = TradeDB()
row = db.conn.execute(
"SELECT payload_json FROM optuna_study_result WHERE study_name=%s",
("momentum_tpe_20260818_20260821_20260823_192516",),
).fetchone()
payload = json.loads(row["payload_json"])
t120 = next(x for x in payload["results_gated"] if x.get("optuna_trial_number") == 120)
combo = dict(t120["params"])
print(
"trial120 ob=%s whip=%s sl=%s tp=%s" % (
combo.get("_orderbook_filter_enabled"),
combo.get("whipsaw_enabled"),
combo.get("sl_pct"),
combo.get("tp_pct"),
),
flush=True,
)
ctx = prepare_momentum_search_context(
start,
end,
"tpe",
orderbook_filter="on",
market="KR",
)
if ctx is None:
print("PREPARE FAILED", flush=True)
return 1
print(
"ctx ready codes=%d ticks=%s ob_codes=%d fee=%s slot=%s max=%s budget=%s"
% (
len(ctx.codes_candles),
bool(ctx.ticks_by_code),
len(ctx.orderbook_by_code or {}),
ctx.fee_rate,
ctx.slot_money,
ctx.max_stocks,
ctx.total_budget_krw,
),
flush=True,
)
base = dict(ctx.base_fixed)
base["_orderbook_filter_enabled"] = True
base["ob_filter_enabled"] = True
grid_keys = list(payload.get("grid_keys") or [])
if not grid_keys:
grid_keys = [k for k in combo.keys() if not str(k).startswith("_")]
result = evaluate_momentum_param_combo(
combo,
base_fixed=base,
grid_keys=grid_keys,
codes_candles=ctx.codes_candles,
min_trades=0,
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=1,
cache_holder=ctx.cache_holder,
ticks_by_code=ctx.ticks_by_code,
orderbook_by_code=ctx.orderbook_by_code,
program_by_code=getattr(ctx, "program_by_code", None),
start_key="20260825",
end_key="20260825",
include_trades=True,
)
print("elapsed_sec", round(time.time() - t0, 1), flush=True)
if not result:
print("NO RESULT", flush=True)
return 2
trades = result.get("_trades") or result.get("trades") or []
out = {
"ok": True,
"date": start,
"source": "optuna trial#120 + prepare orderbook_filter=on",
"study": "momentum_tpe_20260818_20260821_20260823_192516",
"total_trades": result.get("total_trades"),
"win_rate": result.get("win_rate"),
"total_pnl": result.get("total_pnl"),
"pf": result.get("pf"),
"trades": trades,
}
print(
"RESULT trades=%s WR=%s PnL=%s PF=%s"
% (out["total_trades"], out["win_rate"], out["total_pnl"], out["pf"]),
flush=True,
)
for tr in trades:
code = str(tr.get("code") or "")
line = (
" %s->%s %s %s pnl=%s entry=%s exit=%s"
% (
tr.get("buy_time") or tr.get("entry_time"),
tr.get("sell_time") or tr.get("exit_time"),
code,
tr.get("sell_reason"),
tr.get("pnl"),
tr.get("entry_price") or tr.get("buy_price"),
tr.get("exit_price") or tr.get("sell_price"),
)
)
print(line, flush=True)
if code == "417010":
print(" *** NANOTEAM IN BACKTEST ***", flush=True)
out_json.write_text(json.dumps(out, ensure_ascii=False, indent=2, default=str))
print("JSON", out_json, flush=True)
print("DONE", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Optuna 전전략 5-trial 스모크 (LS 틱3차 merge 경로 포함)
# 로그: logs/optuna_smoke_all_YYYYMMDD_HHMMSS.log + 전략별 파일
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
mkdir -p logs
TS="$(date +%Y%m%d_%H%M%S)"
DAY="${1:-2026-08-25}"
TRIALS="${2:-5}"
MASTER="logs/optuna_smoke_all_${TS}.log"
STATUSES="logs/optuna_smoke_status_${TS}.txt"
: >"$STATUSES"
{
echo "======== Optuna smoke start $(date '+%F %T') day=${DAY} trials=${TRIALS} ========"
for S in tail momentum breakout scalp; do
MODE=fast
STUDY="smoke_${S}_${MODE}_${TS}"
LOG="logs/optuna_smoke_${S}_${TS}.log"
echo "----- ${S} study=${STUDY} -----"
set +e
python3 -u kis_trader/backtest/param_search_optuna.py \
--strategy "$S" \
--mode "$MODE" \
--trials "$TRIALS" \
--start "$DAY" \
--end "$DAY" \
--min_trades 1 \
--orderbook-filter off \
--no-progress \
--study-name "$STUDY" \
>"$LOG" 2>&1
RC=$?
set -e
echo "${S} rc=${RC} log=${LOG}" | tee -a "$STATUSES"
echo "----- ${S} done rc=${RC} -----"
# 실패 시그니처
if rg -q "Traceback|Error|❌|KeyError|AttributeError" "$LOG"; then
echo "WARN ${S}: error signature in log" | tee -a "$STATUSES"
rg -n "Traceback|Error|❌|KeyError|AttributeError" "$LOG" | head -40 | tee -a "$STATUSES"
fi
if rg -q "틱 3차 LS|ls_ws_ticks|BT_TICK_LS" "$LOG"; then
echo "INFO ${S}: LS tick path mentioned" | tee -a "$STATUSES"
fi
done
echo "======== Optuna smoke end $(date '+%F %T') ========"
echo "STATUS_FILE=${STATUSES}"
} >>"$MASTER" 2>&1
echo "$MASTER"

View File

@@ -30,9 +30,12 @@ STRATEGIES="${STRATEGIES:-momentum tail breakout scalp}"
# 꼬리 TPE 진입모드 — 한 스터디에 섞지 않음. 공백 구분 시 순차 2회.
# TAIL_OPTUNA_ENTRY_MODES="align limit_atr"
TAIL_OPTUNA_ENTRY_MODES="${TAIL_OPTUNA_ENTRY_MODES:-align}"
# 돌파 TPE 손절모드 — 한 스터디에 섞지 않음. 공백 구분 시 순차 2회.
# 돌파 TPE 손절모드 — 한 스터디에 섞지 않음. 공백 구분 시 순차.
# BREAKOUT_OPTUNA_SL_MODES="fixed atr"
BREAKOUT_OPTUNA_SL_MODES="${BREAKOUT_OPTUNA_SL_MODES:-fixed}"
# 돌파 TPE 호가 스위치 — trial 축 아님. 손절×호가 최대 4순차.
# BREAKOUT_OPTUNA_OB_MODES="off on"
BREAKOUT_OPTUNA_OB_MODES="${BREAKOUT_OPTUNA_OB_MODES:-off}"
# kiwoom|ls — 웹 Optuna 이력소스 / CLI UNIVERSE_HISTORY_SOURCE
UNIVERSE_HISTORY_SOURCE="${UNIVERSE_HISTORY_SOURCE:-${BACKTEST_UNIVERSE_HISTORY_SOURCE:-kiwoom}}"
PY="${PY:-.venv/bin/python}"
@@ -45,9 +48,10 @@ MASTER="logs/optuna_4strat_tpe_${START}_${END}_${TS0}_master.log"
echo "STRATEGIES=$STRATEGIES"
echo "TAIL_OPTUNA_ENTRY_MODES=$TAIL_OPTUNA_ENTRY_MODES"
echo "BREAKOUT_OPTUNA_SL_MODES=$BREAKOUT_OPTUNA_SL_MODES"
echo "BREAKOUT_OPTUNA_OB_MODES=$BREAKOUT_OPTUNA_OB_MODES"
echo "UNIVERSE_HISTORY_SOURCE=$UNIVERSE_HISTORY_SOURCE"
echo "min_wr=$MIN_WIN_RATE min_pf=$MIN_PF min_trades=$MIN_TRADES"
echo "apply-best=OFF orderbook=off n_jobs=1 (사후 results_gated + briefing.md)"
echo "apply-best=OFF breakout-orderbook=스위치(스터디별) n_jobs=1 (사후 results_gated + briefing.md)"
echo "master_log=$MASTER"
free -h | sed -n '1,2p'
df -h / | tail -1
@@ -58,7 +62,8 @@ run_one() {
local strat="$1"
local entry_mode="${2:-}"
local sl_mode="${3:-}"
local ts study log sort_by
local ob_mode="${4:-off}"
local ts study log sort_by bo_extra
ts="$(date +%Y%m%d_%H%M%S)"
study="${strat}_tpe_${START//-/}_${END//-/}_${ts}"
log="logs/optuna_${strat}_tpe_${ts}.log"
@@ -67,8 +72,9 @@ run_one() {
log="logs/optuna_${strat}_${entry_mode}_tpe_${ts}.log"
fi
if [[ "$strat" == "breakout" && -n "$sl_mode" ]]; then
study="${strat}_${sl_mode}_tpe_${START//-/}_${END//-/}_${ts}"
log="logs/optuna_${strat}_${sl_mode}_tpe_${ts}.log"
bo_extra="${sl_mode}_ob_${ob_mode}"
study="${strat}_${bo_extra}_tpe_${START//-/}_${END//-/}_${ts}"
log="logs/optuna_${strat}_${bo_extra}_tpe_${ts}.log"
fi
sort_by="pnl"
case "$strat" in
@@ -77,18 +83,23 @@ run_one() {
{
echo ""
echo "-------- [$strat${entry_mode:+/$entry_mode}${sl_mode:+/$sl_mode}] START $(date -Is) study=$study univ=$UNIVERSE_HISTORY_SOURCE --------"
echo "-------- [$strat${entry_mode:+/$entry_mode}${sl_mode:+/$sl_mode}${bo_extra:+/$bo_extra}] START $(date -Is) study=$study univ=$UNIVERSE_HISTORY_SOURCE --------"
} | tee -a "$MASTER"
echo "$log" > "logs/optuna_${strat}_tpe_latest.logpath"
echo "$study" > "logs/optuna_${strat}_tpe_latest.study"
# 웹 진행률: 전역 latest.study 가 이전 전략에 남으면 바가 1번에서 멈춤 → 잡별 파일
if [[ -n "${OPTUNA_SEQ_ACTIVE_FILE:-}" ]]; then
extra="${entry_mode:-${sl_mode:-}}"
if [[ "$strat" == "breakout" && -n "$bo_extra" ]]; then
extra="$bo_extra"
else
extra="${entry_mode:-${sl_mode:-}}"
fi
{
echo "strategy=$strat"
echo "study=$study"
echo "entry_mode=${entry_mode:-}"
echo "sl_mode=${sl_mode:-}"
echo "ob_mode=${ob_mode:-}"
echo "extra=${extra}"
} > "$OPTUNA_SEQ_ACTIVE_FILE"
fi
@@ -104,7 +115,7 @@ run_one() {
--min_trades "$MIN_TRADES"
--min_win_rate "$MIN_WIN_RATE"
--min_pf "$MIN_PF"
--orderbook-filter off
--orderbook-filter "$([[ "$strat" == "breakout" ]] && echo "$ob_mode" || echo off)"
--no-progress
--study-name "$study"
--sort-by "$sort_by"
@@ -132,7 +143,7 @@ run_one() {
set -e
{
echo "-------- [$strat${entry_mode:+/$entry_mode}${sl_mode:+/$sl_mode}] END rc=$rc $(date -Is) --------"
echo "-------- [$strat${entry_mode:+/$entry_mode}${sl_mode:+/$sl_mode}${bo_extra:+/$bo_extra}] END rc=$rc $(date -Is) --------"
echo "LOG=$log"
grep -E 'OPTUNA_RESULT_JSON=|OPTUNA_BRIEFING_MD=|Best trial|optuna_best|❌|KeyError|Traceback' "$log" | tail -n 24 || true
} | tee -a "$MASTER"
@@ -150,7 +161,9 @@ for s in $STRATEGIES; do
done
elif [[ "$s" == "breakout" ]]; then
for sm in $BREAKOUT_OPTUNA_SL_MODES; do
run_one breakout "" "$sm"
for om in $BREAKOUT_OPTUNA_OB_MODES; do
run_one breakout "" "$sm" "$om"
done
done
else
run_one "$s"