diff --git a/backtest_web.py b/backtest_web.py index ad9a8f8..3ab831e 100644 --- a/backtest_web.py +++ b/backtest_web.py @@ -9733,7 +9733,6 @@ def api_optuna_status(job_id: str): return jsonify({"ok": True, "job": owj.refresh_job_status(meta)}) -@app.route("/api/optuna/jobs", methods=["GET"]) @app.route("/api/optuna/topn/", methods=["GET"]) def api_optuna_topn(job_id: str): from kis_trader.backtest import optuna_web_jobs as owj @@ -9757,6 +9756,7 @@ def api_optuna_topn(job_id: str): "n_trading_days": rs.get("n_trading_days") }) +@app.route("/api/optuna/jobs", methods=["GET"]) def api_optuna_jobs(): from kis_trader.backtest import optuna_web_jobs as owj diff --git a/static/css/backtest.css b/static/css/backtest.css index 16632d0..65c1e5f 100644 --- a/static/css/backtest.css +++ b/static/css/backtest.css @@ -437,3 +437,17 @@ .dash-ops-table tr.dash-broker-head.dash-broker-kis td { background: rgba(63, 185, 80, 0.08); } .dash-ops-table tr.dash-ops-group-start td { border-top: 2px solid var(--border); } .dash-ops-table tr.dash-ops-inactive td { opacity: 0.55; } + + + +/* 체크박스 네이티브 스타일 강제 (빈 테두리만 나오지 않게) */ +.form-check-input[type="checkbox"], .opt-cmp-chk { + -webkit-appearance: checkbox !important; + appearance: checkbox !important; + width: 15px !important; + height: 15px !important; + margin: 0 !important; + cursor: pointer !important; + flex-shrink: 0; + accent-color: #58a6ff; +} diff --git a/static/js/backtest.js b/static/js/backtest.js index 8f620f8..c404557 100644 --- a/static/js/backtest.js +++ b/static/js/backtest.js @@ -3782,7 +3782,8 @@ async function usmomStockOptunaView(source, rank) { strategy: j.strategy || 'us_momentum', start: j.start, end: j.end, - params: j.params_full || j.params_preview || {}, + params: optunaCandidateFormParams(j), + params_engine: j.params_full || j.params_preview || {}, }; usmomStockOptunaRenderResults(window._usmomOptLastSummary); } catch (e) { @@ -3916,7 +3917,8 @@ async function usmomStockOptunaFillForm(source, rank) { strategy: j.strategy || 'us_momentum', start: j.start, end: j.end, - params: j.params_full || j.params_preview || {}, + params: optunaCandidateFormParams(j), + params_engine: j.params_full || j.params_preview || {}, }; // 이미 🇺🇸탭 — tab.click 스킵 (핀 cfg 재로드로 덮어쓰기 방지) await optunaFillFormFromCandidate(src, rk); @@ -8943,9 +8945,6 @@ function optunaObWhipCell(r, stats) { if (on == null && whip == null && !hasExit && !r.ob_summary) { return ''; } - const spr = r.ob_spread != null ? Number(r.ob_spread).toFixed(1) : null; - const ratio = r.ob_ratio != null ? Number(r.ob_ratio).toFixed(2) : null; - const ask = r.ob_ask != null ? Number(r.ob_ask).toFixed(0) : null; const tip = (Array.isArray(r.ob_summary_lines) && r.ob_summary_lines.length) ? r.ob_summary_lines.join(' | ') : (r.ob_summary || '본 TPE 호가·익절·손절·휩쏘'); @@ -8953,9 +8952,9 @@ function optunaObWhipCell(r, stats) { const lines = []; if (on === true) { const bits = ['호가ON']; - if (spr != null) bits.push(`spr${spr}`); - if (ratio != null) bits.push(`r${ratio}`); - if (ask != null) bits.push(`ask×${ask}`); + if (r.ob_spread != null) bits.push(formatWhipParam(r.ob_spread, 'spr', '', 1, 'ob_spread', stats)); + if (r.ob_ratio != null) bits.push(formatWhipParam(r.ob_ratio, 'r', '', 2, 'ob_ratio', stats)); + if (r.ob_ask != null) bits.push(formatWhipParam(r.ob_ask, 'ask×', '', 0, 'ob_ask', stats)); lines.push(`
${bits.join(' ')}
`); } else if (on === false) { lines.push('
호가OFF
'); @@ -8963,26 +8962,18 @@ function optunaObWhipCell(r, stats) { if (hasExit) { const eb = []; - if (r.tp_pct != null) eb.push(`익절${Number(r.tp_pct).toFixed(1)}%`); - if (r.tp_max_pct != null) eb.push(`상한${Number(r.tp_max_pct).toFixed(1)}%`); - if (r.sl_pct != null) eb.push(`손절${Number(r.sl_pct).toFixed(1)}%`); + if (r.tp_pct != null) eb.push(formatWhipParam(r.tp_pct, '익절', '%', 1, 'tp_pct', stats)); + if (r.tp_max_pct != null) eb.push(formatWhipParam(r.tp_max_pct, '상한', '%', 1, 'tp_max_pct', stats)); + if (r.sl_pct != null) eb.push(formatWhipParam(r.sl_pct, '손절', '%', 1, 'sl_pct', stats)); lines.push(`
${eb.join(' ')}
`); } if (whip === true) { const wb = ['휩쏘ON']; - if (r.whip_subbar_sec != null) wb.push(`sub${Number(r.whip_subbar_sec)}s`); - if (r.whip_lookback_sec != null) wb.push(`lb${Number(r.whip_lookback_sec)}s`); - if (r.whip_dip_pct != null) { - let d = Number(r.whip_dip_pct); - if (d < 0.5) d *= 100; - wb.push(`dip${d.toFixed(2)}%`); - } - if (r.whip_recovery_tol_pct != null) { - let t = Number(r.whip_recovery_tol_pct); - if (t < 0.5) t *= 100; - wb.push(`tol${t.toFixed(2)}%`); - } + if (r.whip_subbar_sec != null) wb.push(formatWhipParam(r.whip_subbar_sec, 'sub', 's', 0, 'whip_subbar_sec', stats)); + if (r.whip_lookback_sec != null) wb.push(formatWhipParam(r.whip_lookback_sec, 'lb', 's', 0, 'whip_lookback_sec', stats)); + if (r.whip_dip_pct != null) wb.push(formatWhipParam(r.whip_dip_pct, 'dip', '%', 2, 'whip_dip_pct', stats, true)); + if (r.whip_recovery_tol_pct != null) wb.push(formatWhipParam(r.whip_recovery_tol_pct, 'tol', '%', 2, 'whip_recovery_tol_pct', stats, true)); lines.push(`
${wb.join(' ')}
`); } else if (whip === false && (on != null || hasExit)) { lines.push('
휩쏘OFF
'); @@ -9488,6 +9479,14 @@ function optunaHighlightGated(rank) { optunaRenderPostprocess(window._optunaLastSummary || null); } +/** Optuna candidate API → 웹 폼용 params (꼬리 등 engine↔UI 변환은 서버 params_ui 우선) */ +function optunaCandidateFormParams(j) { + if (!j || typeof j !== 'object') return {}; + const ui = j.params_ui; + if (ui && typeof ui === 'object' && Object.keys(ui).length) return ui; + return j.params_full || j.params_preview || {}; +} + async function optunaShowCandidate(source, rank) { if (!_optunaJobId) { alert('선택된 job 없음 — 최근 잡에서「보기」로 job을 먼저 고르세요'); @@ -9548,7 +9547,8 @@ async function optunaShowCandidate(source, rank) { strategy: j.strategy, start: j.start, end: j.end, - params: j.params_full || j.params_preview || {}, + params: optunaCandidateFormParams(j), + params_engine: j.params_full || j.params_preview || {}, }; // Top5 행 하이라이트 document.querySelectorAll('#opt_top5_tbody tr').forEach((tr, i) => { @@ -9869,7 +9869,8 @@ async function optunaBacktestFromCandidate(source, rank) { strategy: j.strategy, start: j.start, end: j.end, - params: j.params_full || j.params_preview || {}, + params: optunaCandidateFormParams(j), + params_engine: j.params_full || j.params_preview || {}, metrics: j.metrics || {}, }; const cache = window._optunaCandidateCache; @@ -9967,7 +9968,7 @@ async function optunaBacktestFromCandidate(source, rank) { `${tabLabel[strat] || strat} 백테 탭에 반영했습니다.\n` + `trial #${trialNo} · ${src} #${rk}\n` + `기간 ${cache.start || ''}~${cache.end || ''}\n` + - '「백테 실행」으로 Optuna 숫자와 비교하세요.\n(DB 저장 아님)' + '호가/휩쏘 등 후처리 파라미터는 DB 기반으로 동작합니다.\n정확한 수익금 비교를 원하시면 Optuna 리스트에서 [적용(DB저장)]을 먼저 누른 후 백테스트를 실행하세요.' ); } catch (e) { alert('백테 폼 반영 오류: ' + e); @@ -11004,7 +11005,7 @@ let optCmpModalInst = null; async function compareSelectedJobs() { const chks = document.querySelectorAll('.opt-cmp-chk:checked'); if (chks.length < 2) { - alert('비교할 잋을 2개 이상 선택해주세요.'); + alert('비교할 항목을 2개 이상 선택해주세요.'); return; } const jobIds = Array.from(chks).map(c => c.value); @@ -11028,7 +11029,7 @@ async function compareSelectedJobs() { } catch(e) { console.warn('topn fetch failed for', jid, e); } } if (!jobData.length) { - document.getElementById('optCmpTableWrap').innerHTML = '
불러온 잋 데이터가 없습니다.
'; + document.getElementById('optCmpTableWrap').innerHTML = '
불러온 항목 데이터가 없습니다.
'; return; } diff --git a/templates/backtest.html b/templates/backtest.html index d1584b2..31a6f3d 100644 --- a/templates/backtest.html +++ b/templates/backtest.html @@ -6,6 +6,7 @@ 📈 KIS Quant 백테스트 대시보드 + @@ -4049,6 +4050,29 @@ @@ -4483,6 +4485,27 @@ - + + + + \ No newline at end of file