feat(backtest): 대대적인 Optuna 백테스트 웹 UI 및 백엔드 파이프라인 개편

- Web UI:
  - Optuna 탭 추가 및 mode_combo (최빈값 조합), 사후합격 Top 10 시각화 기능
  - 파라미터 분포(p25~p75, median, mode) 히스토그램 및 과적합(Overfit) 위험도 진단 UI 신설
  - 체크박스 렌더링 깨짐 현상을 네이티브(appearance: auto)로 강제 복구 (CSS)
  - 다단 트레일링 스탑, 꼬리 진입/돌파 손절 등 고급 조건 설정 폼 UI 고도화

- Backend (Optuna Jobs):
  - CLI 환경에서 구동된 Optuna json 결과물을 웹 대시보드로 읽어오는 import 기능 강화
  - JSON 메타데이터에 sort_by, mode, 호가 적용 여부 등 핵심 파라미터 파싱 누락 수정
  - optuna_mode_combo.py 등 최빈값 조합 및 후보군 2차 검증을 위한 신규 모듈 추가

- DB & Execution:
  - WebSocket 호가/틱 피드 수집 통계(api_feed_collect_stats) 메모리 캐시 최적화
  - KIS client 접속 키(approval_key) 등 인프라스트럭처 안정성 및 공유 관리 구조 개선
  - 테스트 및 디버깅용 briefing 마크다운 자동 생성 기능 추가
This commit is contained in:
Your Name
2026-09-01 02:47:51 +09:00
parent 3d0255519e
commit 9ba9ab73b6
92 changed files with 8211 additions and 487 deletions

View File

@@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""
scripts/adhoc_cancelable_reconcile_smoke.py
===========================================
Worker SELL C+D — 주말 구조 스모크 (REST 주문·cancel 호출 없음).
검증:
· orders broker debug 컬럼 migrate
· update_order_broker_snapshot / get_latest_sell_order_for_code 헬퍼
· OrderManager._reconcile_cancelable_open disabled 분기 (noop)
· poll 만료 cancel 실패 시 CANCELLED 금지 소스 앵커
실행:
python3 -u scripts/adhoc_cancelable_reconcile_smoke.py
"""
from __future__ import annotations
import inspect
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
REQUIRED_ORDER_COLS = (
"broker_open_qty",
"broker_open_odno",
"broker_reconcile_at",
"broker_reconcile_note",
)
def main() -> int:
ok = True
print("=== adhoc_cancelable_reconcile_smoke ===")
from kis_trader.database.db_manager import get_db
from kis_trader.execution.order_manager import OrderManager, OrderRequest
db = get_db()
db._ensure_orders_table()
cols = [r["Field"] for r in db.conn.execute(
"SHOW COLUMNS FROM orders",
).fetchall()]
missing = [c for c in REQUIRED_ORDER_COLS if c not in cols]
if missing:
print(f"FAIL orders 컬럼 누락: {missing}")
ok = False
else:
print(f"OK orders broker debug 컬럼 {len(REQUIRED_ORDER_COLS)}개 확인")
snap_ok = db.update_order_broker_snapshot(
ord_no="__smoke__",
strategy_id="SMOKE",
code="000000",
open_qty=0,
open_odno=None,
note="adhoc_smoke",
)
if snap_ok:
print("OK update_order_broker_snapshot (no-row UPDATE 허용)")
else:
print("WARN update_order_broker_snapshot False (no matching row — migrate OK)")
mock_client = MagicMock()
mock_db = MagicMock()
mock_db.get_latest_sell_order_for_code.return_value = None
om = OrderManager(client=mock_client, db=mock_db)
req = OrderRequest(
strategy_id="SMOKE",
code="232680",
name="232680",
side="SELL",
qty=1,
price_ref=0.0,
reason="smoke",
)
def _env_bool(key, default=True):
if key == "CANCELABLE_RECONCILE_ENABLED":
return False
if key == "CANCELABLE_RECONCILE_CANCEL_RETRY":
return False
return default
with patch(
"kis_trader.execution.order_manager.get_env_bool",
side_effect=_env_bool,
):
action = om._reconcile_cancelable_open(
req, {"232680": {"psbl_qty": 10, "odno": "X"}},
)
if action != "disabled":
print(f"FAIL reconcile disabled 기대=disabled 실제={action}")
ok = False
else:
print("OK _reconcile_cancelable_open disabled → noop 경로")
from kis_trader.execution import order_manager as om_mod
src = inspect.getsource(om_mod.OrderManager.poll_pending_fills)
if "cancel_failed_at_poll" in src and "CANCELLED 금지" in src:
print("OK poll_pending_fills cancel 실패 → CANCELLED 금지 앵커")
else:
print("FAIL poll_pending_fills cancel-fail 가드 소스 누락")
ok = False
from kis_trader.strategies import base as base_mod
base_src = inspect.getsource(base_mod.BaseStrategy._enqueue_sell)
if "SELL_LOCKED_ENQUEUE_COOLDOWN_SEC" in base_src:
print("OK base._enqueue_sell sell_locked cooldown 앵커")
else:
print("FAIL base enqueue cooldown 소스 누락")
ok = False
ow_src = Path(ROOT / "kis_trader/execution/order_worker.py").read_text(encoding="utf-8")
if "prefetch_broker_holdings(force=False)" in ow_src:
print("OK order_worker prefetch force=False")
else:
print("FAIL order_worker prefetch force=False 누락")
ok = False
print("=== 최종:", "통과" if ok else "실패", "===")
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -38,6 +38,9 @@ BREAKOUT_OPTUNA_SL_MODES="${BREAKOUT_OPTUNA_SL_MODES:-fixed}"
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}}"
# 웹 순차 잡 ID (전략/조합별 1·2차 state 파일 prefix)
OPTUNA_SEQ_JOB_ID="${OPTUNA_SEQ_JOB_ID:-seq}"
SORT_BY="${SORT_BY:-score}"
PY="${PY:-.venv/bin/python}"
TS0="$(date +%Y%m%d_%H%M%S)"
MASTER="logs/optuna_4strat_tpe_${START}_${END}_${TS0}_master.log"
@@ -51,7 +54,7 @@ MASTER="logs/optuna_4strat_tpe_${START}_${END}_${TS0}_master.log"
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 breakout-orderbook=스위치(스터디별) n_jobs=1 (사후 results_gated + briefing.md)"
echo "apply-best=OFF · 전략/조합마다 1·2차 TPE 연쇄 · n_jobs=1 (사후 results_gated + briefing.md)"
echo "master_log=$MASTER"
free -h | sed -n '1,2p'
df -h / | tail -1
@@ -63,97 +66,106 @@ run_one() {
local entry_mode="${2:-}"
local sl_mode="${3:-}"
local ob_mode="${4:-off}"
local ts study log sort_by bo_extra
local ts step_job_id refine_log state_file sort_by bo_extra extra min_trades_arg
ts="$(date +%Y%m%d_%H%M%S)"
study="${strat}_tpe_${START//-/}_${END//-/}_${ts}"
log="logs/optuna_${strat}_tpe_${ts}.log"
if [[ "$strat" == "tail" && -n "$entry_mode" ]]; then
study="${strat}_${entry_mode}_tpe_${START//-/}_${END//-/}_${ts}"
log="logs/optuna_${strat}_${entry_mode}_tpe_${ts}.log"
fi
if [[ "$strat" == "breakout" && -n "$sl_mode" ]]; then
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="${SORT_BY:-score}"
case "$sort_by" in
score|score_legacy|pnl|daily_avg|win_rate) ;;
*) sort_by="score" ;;
esac
local min_trades_arg
if [[ "$strat" == "breakout" && -n "$sl_mode" ]]; then
bo_extra="${sl_mode}_ob_${ob_mode}"
extra="$bo_extra"
elif [[ "$strat" == "tail" && -n "$entry_mode" ]]; then
extra="$entry_mode"
else
extra=""
fi
step_job_id="${OPTUNA_SEQ_JOB_ID}_${strat}${extra:+_${extra}}_${ts}"
refine_log="logs/optuna_seq_refine_${step_job_id}.log"
state_file="logs/${step_job_id}_refine_state.json"
min_trades_arg=$("$PY" -c "from kis_trader.backtest.optuna_common import resolve_optuna_min_trades; print(resolve_optuna_min_trades('${START}', '${END}', '${strat}')['min_trades'])")
{
echo ""
echo "-------- [$strat${entry_mode:+/$entry_mode}${sl_mode:+/$sl_mode}${bo_extra:+/$bo_extra}] START $(date -Is) study=$study univ=$UNIVERSE_HISTORY_SOURCE min_trades=$min_trades_arg --------"
echo "-------- [$strat${entry_mode:+/$entry_mode}${sl_mode:+/$sl_mode}${bo_extra:+/$bo_extra}] START $(date -Is) 1·2차 step=$step_job_id --------"
} | 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
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}"
echo "refine_state_path=$state_file"
} > "$OPTUNA_SEQ_ACTIVE_FILE"
fi
set +e
set +e
local cmd_args=(
local refine_args=(
--job-id "$step_job_id"
--strategy "$strat"
--mode "$MODE"
--start "$START"
--end "$END"
--trials "$TRIALS"
--min_trades "$min_trades_arg"
--min_win_rate "$MIN_WIN_RATE"
--min_pf "$MIN_PF"
--orderbook-filter "$([[ "$strat" == "breakout" ]] && echo "$ob_mode" || echo off)"
--no-progress
--study-name "$study"
--sort-by "$sort_by"
--min-trades "$min_trades_arg"
--universe-history-source "$UNIVERSE_HISTORY_SOURCE"
)
if [[ -n "${STUDY_TRIALS:-}" && "${STUDY_TRIALS}" != "0" ]]; then
cmd_args+=(--study-trials "$STUDY_TRIALS")
refine_args+=(--study-trials "$STUDY_TRIALS")
fi
if [[ "$strat" == "tail" && -n "$entry_mode" ]]; then
cmd_args+=(--entry-mode "$entry_mode")
refine_args+=(--entry-mode "$entry_mode")
fi
if [[ "$strat" == "breakout" && -n "$sl_mode" ]]; then
cmd_args+=(--sl-mode "$sl_mode")
refine_args+=(--sl-mode "$sl_mode" --ob-mode "$ob_mode")
fi
if [[ -n "${CANDLE_SOURCE:-}" ]]; then
refine_args+=(--candle-source "$CANDLE_SOURCE")
fi
if [[ -n "${TICK_SOURCE:-}" ]]; then
cmd_args+=("--tick-source" "$TICK_SOURCE")
refine_args+=(--tick-source "$TICK_SOURCE")
fi
if [[ -n "${OB_SOURCE:-}" ]]; then
cmd_args+=("--ob-source" "$OB_SOURCE")
refine_args+=(--ob-source "$OB_SOURCE")
fi
"$PY" -u kis_trader/backtest/param_search_optuna.py "${cmd_args[@]}" >"$log" 2>&1
"$PY" -u kis_trader/backtest/optuna_mode_refine_runner.py "${refine_args[@]}" >"$refine_log" 2>&1
local rc=$?
set -e
local result_json briefing_md
result_json=""
briefing_md=""
if [[ -f "$state_file" ]]; then
result_json=$("$PY" -c "import json; print(json.load(open('${state_file}')).get('result_json') or '')" 2>/dev/null || true)
if [[ -n "$result_json" && -f "$result_json" ]]; then
echo "OPTUNA_RESULT_JSON=$result_json" | tee -a "$MASTER"
briefing_md="${result_json%.json}.briefing.md"
if [[ -f "$briefing_md" ]]; then
echo "OPTUNA_BRIEFING_MD=$briefing_md" | tee -a "$MASTER"
fi
fi
fi
if [[ -z "$result_json" ]]; then
grep -E 'OPTUNA_RESULT_JSON=' "$refine_log" | tail -n 1 | tee -a "$MASTER" || true
fi
{
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
echo "REFINE_LOG=$refine_log"
echo "STATE=$state_file"
grep -E 'OPTUNA_RESULT_JSON=|OPTUNA_BRIEFING_MD=|✅ 1·2차|❌ 1·2차|phase1|phase2|Traceback' "$refine_log" | tail -n 24 || true
} | tee -a "$MASTER"
if [[ "$rc" -ne 0 ]]; then
echo "⚠️ [$strat] 실패 rc=$rc — 다음 전략 계속" | tee -a "$MASTER"
echo "⚠️ [$strat] 1·2차 실패 rc=$rc — 다음 전략 계속" | tee -a "$MASTER"
fi
return 0
}