Files
kis_bot/scratch/revert_ide.patch

580 lines
29 KiB
Diff
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
--- a/kis_trader/backtest/optuna_web_jobs.py
+++ b/kis_trader/backtest/optuna_web_jobs.py
@@ -448,7 +448,6 @@
"join_cmd_ps": m.get("join_cmd_ps") or "",
"web_cmd": m.get("web_cmd") or m.get("cmd") or "",
"web_cmd_full": m.get("web_cmd_full") or "",
- "mode": m.get("mode"),
"join_study": m.get("join_study") or m.get("active_study_name") or m.get("study_name") or "",
"join_hint": m.get("join_hint") or "",
"join_cmds_all": m.get("join_cmds_all") or [],
@@ -459,17 +459,6 @@
str(m.get("study_name") or m.get("active_study_name") or "")
),
"source": m.get("source"),
- "sort_by": m.get("sort_by"),
- "symbol": m.get("symbol"),
- "universe_history_source": m.get("universe_history_source"),
- "candle_source": m.get("candle_source"),
- "tick_source": m.get("tick_source"),
- "ob_source": m.get("ob_source"),
- "tail_entry_modes": m.get("tail_entry_modes"),
- "breakout_sl_modes": m.get("breakout_sl_modes"),
- "breakout_ob_modes": m.get("breakout_ob_modes"),
- # result_summary에서 추출한 ob 축 포함 여부 (sort_by가 없는 구잡 대응)
- "tpe_includes_orderbook": (m.get("result_summary") or {}).get("tpe_includes_orderbook"),
}
@@ -2836,7 +2836,6 @@
"label": label,
"strategy": strat,
"mode": mode,
- "sort_by": data.get("sort_by") or meta.get("sort_by"),
"start": start,
"end": end,
"trials": int(data.get("optuna_trials_completed") or data.get("optuna_n_trials_requested") or 0),
--- a/static/js/backtest.js
+++ b/static/js/backtest.js
@@ -8430,7 +8430,7 @@
optunaRenderParamDistChart(sum);
}
-const OPTUNA_CARD_TOP_N = 10;
+const OPTUNA_CARD_TOP_N = 5;
function optunaRenderModeCombo(sum) {
const el = $('opt_mode_combo_body');
@@ -8640,7 +8640,7 @@
return '';
}
-function optunaFormatGatedTopRowHtml(t, i, showBorder, stats) {
+function optunaFormatGatedTopRowHtml(t, i, showBorder) {
const dAvg = (t.period_daily_avg_pnl != null) ? t.period_daily_avg_pnl : t.daily_pnl_mean;
const dPct = (t.period_daily_avg_pct != null) ? t.period_daily_avg_pct : t.daily_avg_pct;
const rank = t.rank || i;
@@ -8651,7 +8651,7 @@
${dAvg != null ? `<span>일평균 ${Number(dAvg).toLocaleString()}원</span>` : ''}
${dPct != null ? `<span>일평균 ${Number(dPct) >= 0 ? '+' : ''}${Number(dPct).toFixed(3)}%</span>` : ''}
<span>WR ${Number(t.win_rate || 0).toFixed(1)}% · PF ${Number(t.pf || 0).toFixed(2)} · ${Number(t.total_trades || 0)}건</span>
- <span>${optunaObWhipCell(t, stats)}</span>
+ <span>${optunaObWhipCell(t)}</span>
<button type="button" class="btn btn-sm btn-outline-secondary py-0" onclick="optunaShowCandidate('gated',${rank})">보기</button>
<button type="button" class="btn btn-sm btn-outline-primary py-0" onclick="optunaBacktestFromCandidate('gated',${rank})">백테</button>
<button type="button" class="btn btn-sm btn-outline-success py-0" onclick="optunaApply('gated',${rank})">적용</button>
@@ -8658,55 +8658,5 @@
}
-function optunaCalcStats(list) {
- const keys = ['ob_spread', 'ob_ratio', 'ob_ask', 'tp_pct', 'tp_max_pct', 'sl_pct', 'whip_subbar_sec', 'whip_lookback_sec', 'whip_dip_pct', 'whip_recovery_tol_pct'];
- const stats = {};
- keys.forEach(k => {
- let sum = 0, cnt = 0, min = Infinity, max = -Infinity;
- list.forEach(r => {
- if (r && r[k] != null) {
- let v = Number(r[k]);
- if ((k === 'whip_dip_pct' || k === 'whip_recovery_tol_pct') && v < 0.5) v *= 100;
- sum += v; cnt++;
- if (v < min) min = v;
- if (v > max) max = v;
- }
- });
- if (cnt > 0) {
- const avg = sum / cnt;
- let maxDist = Math.max(Math.abs(max - avg), Math.abs(min - avg));
- stats[k] = { avg, min, max, maxDist };
- }
- });
- return stats;
-}
-
-function optunaFormatAvgHtml(stats, len) {
- if (!stats || Object.keys(stats).length === 0) return '';
- const avgFmt = (k, prefix, suffix, dec) => {
- if (!stats[k]) return '';
- return `${prefix}${stats[k].avg.toFixed(dec)}${suffix} `;
- };
- return `<div class="mb-2 p-2 text-start" style="background:var(--bg-card);border:1px solid var(--border);border-radius:6px;font-size:11px;">
- <div class="fw-bold mb-1" style="color:hotpink;">수치 평균 (Top${len} 기준 · 근접시 진하게)</div>
- <div class="text-muted">
- ${stats['ob_spread'] ? `호가ON ` : ''}
- ${avgFmt('ob_spread', 'spr', '', 1)}
- ${avgFmt('ob_ratio', 'r', '', 2)}
- ${avgFmt('ob_ask', 'ask×', '', 1)}
- <br>
- ${avgFmt('tp_pct', '익절', '%', 1)}
- ${avgFmt('tp_max_pct', '상한', '%', 1)}
- ${avgFmt('sl_pct', '손절', '%', 1)}
- <br>
- ${stats['whip_subbar_sec'] ? `휩쏘ON ` : ''}
- ${avgFmt('whip_subbar_sec', 'sub', 's', 0)}
- ${avgFmt('whip_lookback_sec', 'lb', 's', 0)}
- ${avgFmt('whip_dip_pct', 'dip', '%', 2)}
- ${avgFmt('whip_recovery_tol_pct', 'tol', '%', 2)}
- </div>
- </div>`;
-}
-
function optunaBuildGatedTop3Html(rows, opts) {
const o = opts || {};
const n = o.topN || OPTUNA_CARD_TOP_N;
@@ -8668,13 +8668,9 @@
const note = o.running
? ' <span class="text-warning">(진행 중 · 사후합격 미확정)</span>'
: '';
-
- const stats = optunaCalcStats(list);
let html = `<div class="mb-1 fw-semibold text-success">${phase} Top${n}${pr}${note}</div>`;
- html += optunaFormatAvgHtml(stats, list.length);
-
- list.forEach((t, i) => {
- html += optunaFormatGatedTopRowHtml(t, i + 1, i < list.length - 1, stats);
+ list.forEach((t, i) => {
+ html += optunaFormatGatedTopRowHtml(t, i + 1, i < list.length - 1);
});
return html;
}
@@ -8771,13 +8771,6 @@
const poolNote = d.threshold_pool
? `<div class="text-muted mb-2" style="font-size:11px">표본: ${d.threshold_pool} · n=${d.threshold_pool_n || 0}</div>`
: '';
- const legendHtml = `<div class="mb-3 p-2" style="background:rgba(255,255,255,0.04);border-radius:6px;font-size:11px;line-height:1.7;">
- <b style="font-size:12px;">📊 분포 차트 읽는 법</b><br>
- <span style="display:inline-block;width:28px;height:8px;background:rgba(56,139,253,0.35);border-radius:3px;vertical-align:middle;margin-right:4px;"></span>파란 띠 = <b>p25 ~ p75</b> 구간 (상위합격 trial 중 중간 50% · IQR)<br>
- <span style="display:inline-block;width:10px;height:10px;background:#3fb950;border-radius:2px;vertical-align:middle;margin-right:4px;"></span>녹색 선 = <b>median</b> (중앙값 · 합격 trial 중 딱 50번째 백분위)<br>
- <span style="display:inline-block;width:10px;height:10px;background:#f0883e;border-radius:50%;vertical-align:middle;margin-right:4px;"></span>주황 점 = <b>mode</b> (최빈값 · DB 적용 후보에서 가장 많이 선택된 값)<br>
- <span style="color:#aaa;">💡 주황(mode)과 녹색(median)이 가까울수록 분포가 안정적. 파란띠가 넓으면 파라미터 선택 폭이 큼. 녹색이 중앙에서 치우치는 건 분포가 한쪽으로 몰린 비대칭(skew) 때문.</span>
- </div>`;
const bars = rows.map((r) => {
const p25 = Number(r.p25);
const p75 = Number(r.p75);
@@ -8784,12 +8784,9 @@
const mode = r.mode != null ? Number(r.mode) : null;
const vals = [p25, p75, med, mode].filter((v) => v != null && !Number.isNaN(v));
- const lo = Math.min(...vals);
- const hi = Math.max(...vals);
- const pad = (hi - lo) * 0.12 || Math.abs(lo) * 0.1 || 0.5;
- const rangeMin = lo - pad;
- const rangeMax = hi + pad;
- const span = rangeMax - rangeMin || 1;
- const pct = (v) => Math.max(0, Math.min(100, ((v - rangeMin) / span) * 100));
+ const lo = Math.min(...vals) * 0.92;
+ const hi = Math.max(...vals) * 1.08;
+ const span = hi - lo || 1;
+ const pct = (v) => Math.max(0, Math.min(100, ((v - lo) / span) * 100));
const share = r.mode_share != null ? `${(Number(r.mode_share) * 100).toFixed(0)}%` : '—';
const fmt = (v) => (v == null || Number.isNaN(v)) ? '—' : optunaFmtNum(v, 3);
let marks = '';
@@ -8796,25 +8796,23 @@
- marks += `<div style="position:absolute;left:calc(${pct(med).toFixed(1)}% - 1px);top:0;width:2px;height:100%;background:#3fb950;z-index:2;" title="median ${med}"></div>`;
- marks += `<div style="position:absolute;left:${pct(med).toFixed(1)}%;top:-14px;transform:translateX(-50%);font-size:9px;color:#3fb950;white-space:nowrap;">${fmt(med)}</div>`;
+ marks += `<div style="position:absolute;left:${pct(med).toFixed(1)}%;top:0;width:2px;height:100%;background:#3fb950" title="median ${med}"></div>`;
}
if (mode != null && !Number.isNaN(mode)) {
- marks += `<div style="position:absolute;left:calc(${pct(mode).toFixed(1)}% - 4px);top:3px;width:8px;height:8px;background:#f0883e;border-radius:50%;z-index:3;" title="mode ${mode}"></div>`;
+ marks += `<div style="position:absolute;left:${pct(mode).toFixed(1)}%;top:3px;width:8px;height:8px;background:#f0883e;border-radius:50%" title="mode ${mode}"></div>`;
}
const l = pct(p25).toFixed(1);
const w = Math.max(1, pct(p75) - pct(p25)).toFixed(1);
- return `<div class="mb-3">
- <div class="d-flex justify-content-between align-items-center mb-1">
+ return `<div class="mb-2">
+ <div class="d-flex justify-content-between align-items-center">
<code style="font-size:11px">${r.param}</code>
- <span class="text-muted" style="font-size:10px">mode <b style="color:#f0883e">${fmt(r.mode)}</b> (${share}) · med <b style="color:#3fb950">${fmt(med)}</b> · p25~p75: ${fmt(p25)}~${fmt(p75)}</span>
- </div>
- <div style="position:relative;height:14px;background:#21262d;border-radius:4px;margin:16px 0 4px;">
- <div style="position:absolute;left:${l}%;width:${w}%;height:100%;background:rgba(56,139,253,0.35);border-radius:3px;"></div>
+ <span class="text-muted" style="font-size:10px">mode ${fmt(r.mode)} (${share})</span>
+ </div>
+ <div style="position:relative;height:14px;background:#21262d;border-radius:4px;margin:2px 0">
+ <div style="position:absolute;left:${l}%;width:${w}%;height:100%;background:rgba(56,139,253,0.4);border-radius:3px"></div>
${marks}
- <div style="position:absolute;left:0;top:16px;font-size:9px;color:#555;white-space:nowrap;">${fmt(rangeMin)}</div>
- <div style="position:absolute;right:0;top:16px;font-size:9px;color:#555;white-space:nowrap;">${fmt(rangeMax)}</div>
- </div>
- </div>`;
- }).join('');
- el.innerHTML = poolNote + legendHtml + bars;
+ </div>
+ <div class="text-muted" style="font-size:10px">p25 ${fmt(p25)} · med ${fmt(med)} · p75 ${fmt(p75)}</div>
+ </div>`;
+ }).join('');
+ el.innerHTML = poolNote + bars;
}
function optunaRenderTrailRec(sum) {
@@ -8857,20 +8857,7 @@
return job.status === 'running' && Number(prog.pct || 0) >= 99.9;
}
-function formatWhipParam(vRaw, prefix, suffix, dec, statKey, stats, scale100) {
- if (vRaw == null) return null;
- let v = Number(vRaw);
- if (scale100 && v < 0.5) v *= 100;
- let text = `${prefix}${v.toFixed(dec)}${suffix}`;
- if (!stats || !stats[statKey]) return text;
- const s = stats[statKey];
- const dist = Math.abs(v - s.avg);
- const ratio = s.maxDist > 0 ? 1 - (dist / s.maxDist) : 1;
- const alpha = Math.round((0.2 + (0.8 * ratio)) * 100);
- return `<span style="background: color-mix(in srgb, hotpink ${alpha}%, transparent); color:${alpha > 60 ? '#fff' : 'inherit'}; border-radius:3px; padding:0 3px;">${text}</span>`;
-}
-
-function optunaObWhipCell(r, stats) {
+function optunaObWhipCell(r) {
if (!r) return '<span class="text-muted">—</span>';
const on = r.ob_on;
const whip = r.whip_on;
@@ -8877,6 +8877,9 @@
if (on == null && whip == null && !hasExit && !r.ob_summary) {
return '<span class="text-muted" title="구JSON 또는 호가·익절·손절·휩쏘 축 없음">—</span>';
}
+ 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 호가·익절·손절·휩쏘');
@@ -8883,15 +8883,12 @@
const lines = [];
if (on === true) {
const bits = ['호가ON'];
- const spr = formatWhipParam(r.ob_spread, 'spr', '', 1, 'ob_spread', stats);
- const ratio = formatWhipParam(r.ob_ratio, 'r', '', 2, 'ob_ratio', stats);
- const ask = formatWhipParam(r.ob_ask, 'ask×', '', 0, 'ob_ask', stats);
- if (spr) bits.push(spr);
- if (ratio) bits.push(ratio);
- if (ask) bits.push(ask);
- lines.push(`<div class="text-pnl-pos" style="font-size:10px;line-height:1.4">${bits.join(' ')}</div>`);
+ if (spr != null) bits.push(`spr${spr}`);
+ if (ratio != null) bits.push(`r${ratio}`);
+ if (ask != null) bits.push(`ask×${ask}`);
+ lines.push(`<div class="text-pnl-pos" style="font-size:10px;line-height:1.25">${bits.join(' ')}</div>`);
} else if (on === false) {
- lines.push('<div class="text-muted" style="font-size:10px;line-height:1.4">호가OFF</div>');
+ lines.push('<div class="text-muted" style="font-size:10px;line-height:1.25">호가OFF</div>');
}
if (hasExit) {
@@ -8898,10 +8898,7 @@
- const tp = formatWhipParam(r.tp_pct, '익절', '%', 1, 'tp_pct', stats);
- const tpMax = formatWhipParam(r.tp_max_pct, '상한', '%', 1, 'tp_max_pct', stats);
- const sl = formatWhipParam(r.sl_pct, '손절', '%', 1, 'sl_pct', stats);
- if (tp) eb.push(tp);
- if (tpMax) eb.push(tpMax);
- if (sl) eb.push(sl);
- lines.push(`<div class="text-muted" style="font-size:10px;line-height:1.4">${eb.join(' ')}</div>`);
+ 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)}%`);
+ lines.push(`<div class="text-muted" style="font-size:10px;line-height:1.25">${eb.join(' ')}</div>`);
}
if (whip === true) {
@@ -8908,14 +8908,18 @@
- const sub = formatWhipParam(r.whip_subbar_sec, 'sub', 's', 0, 'whip_subbar_sec', stats);
- const lb = formatWhipParam(r.whip_lookback_sec, 'lb', 's', 0, 'whip_lookback_sec', stats);
- const dip = formatWhipParam(r.whip_dip_pct, 'dip', '%', 2, 'whip_dip_pct', stats, true);
- const tol = formatWhipParam(r.whip_recovery_tol_pct, 'tol', '%', 2, 'whip_recovery_tol_pct', stats, true);
- if (sub) wb.push(sub);
- if (lb) wb.push(lb);
- if (dip) wb.push(dip);
- if (tol) wb.push(tol);
- lines.push(`<div class="text-warning" style="font-size:10px;line-height:1.4">${wb.join(' ')}</div>`);
+ 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)}%`);
+ }
+ lines.push(`<div class="text-warning" style="font-size:10px;line-height:1.25">${wb.join(' ')}</div>`);
} else if (whip === false && (on != null || hasExit)) {
- lines.push('<div class="text-muted" style="font-size:10px;line-height:1.4">휩쏘OFF</div>');
+ lines.push('<div class="text-muted" style="font-size:10px;line-height:1.25">휩쏘OFF</div>');
}
if (!lines.length) {
@@ -9935,10 +9935,7 @@
const el = $(id);
if (!el || val === undefined || val === null) return;
if (el.type === 'checkbox') el.checked = !!(val === true || val === 1 || val === '1');
- else {
- el.style.cssText = '-webkit-appearance:checkbox!important;appearance:checkbox!important;width:15px!important;height:15px!important;min-width:15px;margin:0!important;padding:0!important;cursor:pointer;flex-shrink:0;accent-color:#58a6ff;background:none!important;border:none!important;box-shadow:none!important;float:none!important;';
- el.value = val;
- }
+ else el.value = val;
};
const prefix = isUs ? 'usmom_' : 'mom_';
// 기간 = Optuna study 기간 (비교 시 필수)
@@ -10507,8 +10507,6 @@
` · WR ${Number(t.win_rate || 0).toFixed(1)}% · PF ${Number(t.pf || 0).toFixed(2)} · score ${Number(t.score || 0).toFixed(3)}`;
}
-let _optJobsCache = {}; // job_id -> job 객체 (list API 응답 캐시)
-
async function optunaRefreshJobs() {
try {
const sort = $('opt_jobs_sort')?.value || 'started';
@@ -10518,16 +10518,9 @@
tb.innerHTML = '<tr><td colspan="8" class="text-muted">잡 없음</td></tr>';
return;
}
- const prevChks = document.querySelectorAll('.opt-cmp-chk:checked');
- const checkedIds = new Set(Array.from(prevChks).map(c => c.value));
-
- // 캐시 갱신
- (j.jobs || []).forEach(job => { _optJobsCache[job.job_id] = job; });
-
tb.innerHTML = j.jobs.map(job => {
const prog = job.progress || {};
const jid = job.job_id;
- const isChecked = checkedIds.has(jid) ? 'checked' : '';
const label = job.label || job.strategy || '';
const studyNote = (job.study_short && job.kind === 'import')
? `<br><span class="text-muted" style="font-size:10px" title="${job.study_name || ''}">${job.study_short}</span>`
@@ -10541,31 +10541,9 @@
? `<button class="btn btn-sm btn-outline-primary py-0 ms-1" onclick="optunaContinue('${jid}')">이어 돌리기</button>` +
`<button class="btn btn-sm btn-outline-warning py-0 ms-1" onclick="optunaConfirmStudy('${jid}')">확정</button>`
: '';
-
- const runParams = [];
- // 승리식: sort_by 필드 없으면 표시 생략 (구잡)
- const sortByRaw = job.sort_by || null;
- const sortByLabel = sortByRaw === 'score' ? '수익·낙폭' : sortByRaw === 'score_legacy' ? '구pnl/mdd' : sortByRaw === 'daily_avg' ? '일평균' : sortByRaw === 'pnl' ? '총손익' : sortByRaw;
- if (sortByLabel) runParams.push('승리식:' + sortByLabel);
- // 호가 ON/OFF: 백엔드에서 result_summary.tpe_includes_orderbook을 슬림화하여 내려줌
- const obOn = job.tpe_includes_orderbook;
- if (obOn === true) runParams.push('호가:ON');
- if (obOn === false) runParams.push('호가:OFF');
- if (job.mode && job.mode !== 'tpe') runParams.push('모드:' + job.mode);
- if (job.symbol) runParams.push('종목:' + job.symbol);
- if (job.universe_history_source) runParams.push('유니버스:' + job.universe_history_source);
- if (job.tail_entry_modes) runParams.push('꼬리진입:' + job.tail_entry_modes);
- if (job.breakout_sl_modes) runParams.push('돌파손절:' + job.breakout_sl_modes);
- if (job.breakout_ob_modes) runParams.push('돌파호가:' + job.breakout_ob_modes);
-
- const paramsDiv = runParams.length > 0 ? `<div style="font-size:10px; color:var(--muted); margin-top:2px; white-space: normal; line-height: 1.3;">${runParams.join(' · ')}</div>` : '';
-
return `<tr data-job-id="${jid}" style="${sel}">
- <td style="padding:4px 6px;vertical-align:middle;">
- <input class="form-check-input opt-cmp-chk" type="checkbox" value="${jid}" ${isChecked} style="margin-top:0; cursor:pointer;">
- <code style="font-size:10px; cursor:pointer; margin-left:4px;" onclick="const cb=this.previousElementSibling; cb.checked=!cb.checked; cb.dispatchEvent(new Event('change'))">${jid}</code>
- </td>
- <td style="max-width:280px; white-space:normal;">${label}${studyNote}${paramsDiv}</td>
+ <td><code style="font-size:10px">${jid}</code></td>
+ <td>${label}${studyNote}</td>
<td>${optunaFormatJobPeriodCell(job)}</td>
<td>${stCell}${job.phase === 'postprocess' ? ' ·후처리' : ''}${statusNote}</td>
<td>${(() => {
@@ -10886,131 +10886,4 @@
optunaBoot();
kisMockBoot();
btRestoreTab();
-
-let optCmpModalInst = null;
-
-async function compareSelectedJobs() {
- const chks = document.querySelectorAll('.opt-cmp-chk:checked');
- if (chks.length < 2) {
- alert('\ube44\uad50\ud560 \uc78b\uc744 2\uac1c \uc774\uc0c1 \uc120\ud0dd\ud574\uc8fc\uc138\uc694.');
- return;
- }
- const jobIds = Array.from(chks).map(c => c.value);
-
- const modalEl = document.getElementById('optCmpModal');
- if (!optCmpModalInst) {
- optCmpModalInst = new bootstrap.Modal(modalEl);
- }
- document.getElementById('optCmpLoading').classList.remove('d-none');
- document.getElementById('optCmpTableWrap').innerHTML = '';
- optCmpModalInst.show();
-
- try {
- const jobData = [];
- // 항상 topn API 호출 (리스트 API에는 top5_gated가 없기 때문)
- for (const jid of jobIds) {
- try {
- const r = await fetch('/api/optuna/topn/' + encodeURIComponent(jid), {signal: AbortSignal.timeout(5000)});
- const j2 = await r.json();
- if (j2.ok) jobData.push(j2);
- } catch(e) { console.warn('topn fetch failed for', jid, e); }
- }
- if (!jobData.length) {
- document.getElementById('optCmpTableWrap').innerHTML = '<div class="text-warning py-3 text-center">\ubd88\ub7ec\uc628 \uc78b \ub370\uc774\ud130\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.</div>';
- return;
- }
-
- const pFmt = v => v != null ? Math.round(Number(v)).toLocaleString() : '\u2014';
- const pRound2 = v => v != null ? Number(v).toFixed(2) : '\u2014';
- const maxTop = 10;
-
- // Header
- let html = '<table class="table table-sm table-bordered" style="font-size:12px;">';
- html += '<thead class="table-dark"><tr><th style="min-width:110px;">\uc9c0\ud45c</th>';
- for (const d of jobData) {
- const gated = d.top5_gated || (d.result_summary && d.result_summary.top5_gated) || [];
- const stats = optunaCalcStats(gated);
- d._stats = stats;
- d._gatedList = gated;
-
- const lbl = (d.label || d.strategy || d.job_id || '').replace(/<[^>]+>/g,'');
- const sn = (d.study_short || '').replace(/<[^>]+>/g,'');
- const sb = d.sort_by || 'score';
- const sbTxt = sb==='score'?'\uc218\uc775\u00b7\ub099\ud3ed':sb==='score_legacy'?'\uad6cpnl/mdd':sb==='daily_avg'?'\uc77c\ud3c9\uade0':sb==='pnl'?'\ucd1d\uc190\uc775':sb;
- const obTxt = d.tpe_includes_orderbook ? '호가:ON' : (d.tpe_includes_orderbook === false ? '호가:OFF' : '');
- const jidShort = (d.job_id||'').replace(/^import_optuna_/, '');
- html += `<th style="min-width:150px;white-space:normal;position:relative;vertical-align:top;">
- <div style="font-weight:700;">${lbl}</div>
- ${sn?`<div style="font-size:10px;color:#aaa;">${sn}</div>`:''}
- <div style="font-size:10px;color:#6ea8fe;">승리식: ${sbTxt} · ${d.mode||'tpe'}</div>
- <div style="font-size:10px;color:#aaa;">${optunaFormatJobPeriodCell(d)}</div>
- <div style="font-size:10px;color:#f0883e;margin-top:2px;">${obTxt}</div>
- <div style="font-size:9px;color:#555;word-break:break-all;margin-top:2px;">${jidShort}</div>
- <div style="margin-top:6px;font-weight:normal;">${optunaFormatAvgHtml(stats, gated.length)}</div>
- </th>`;
- }
- html += '</tr></thead><tbody>';
-
- const metricDefs = [
- { label:'\uc77c\ud3c9\uade0 \uc190\uc775(\uc6d0)', fn: g => {
- const v = g.period_daily_avg_pnl ?? null;
- if (v == null) return '\u2014';
- const cls = v>0?'color:#3fb950':v<0?'color:#f85149':'';
- return `<span style="${cls}">${(v>=0?'+':'') + pFmt(v)}</span>`;
- }},
- { label:'\uc77c\ud3c9\uade0 \uc190\uc775(%)', fn: g => {
- const v = g.period_daily_avg_pct ?? null;
- if (v == null) return '\u2014';
- const cls = v>0?'color:#3fb950':v<0?'color:#f85149':'';
- return `<span style="${cls}">${(v>=0?'+':'')+Number(v).toFixed(3)}%</span>`;
- }},
- { label:'\ucd1d \uc190\uc775(\uc6d0)', fn: g => {
- const v = g.total_pnl ?? null;
- if (v == null) return '\u2014';
- const cls = v>0?'color:#3fb950':v<0?'color:#f85149':'';
- return `<span style="${cls}">${pFmt(v)}</span>`;
- }},
- { label:'\uc2b9\ub960 / PF', fn: g => `${pRound2(g.win_rate)}% / ${pRound2(g.pf)}` },
- { label:'\uac70\ub798\uc218 / trial', fn: g => `${g.total_trades??'\u2014'}\uac74 / #${g.optuna_trial_number??'\u2014'}` },
- { label:'\uc775\uc808 tp(%)', fn: g => {
- const v = g.tp_pct ?? null;
- return v!=null?`<span style="color:#79c0ff">${v}%</span>`:'\u2014';
- }},
- { label:'\uc190\uc808 sl(%)', fn: g => {
- const v = g.sl_pct ?? null;
- return v!=null?`<span style="color:#f85149">${v}%</span>`:'\u2014';
- }},
- { label:'\ud638\uac00 ON', fn: (g) => g.ob_on!=null?(g.ob_on?'\u2705':'\u274c'):'\u2014' },
- { label:'파라미터<br><span style="font-size:10px;color:#555;">(호가·익손절·휩쏘)</span>', fn: (g, d) => optunaObWhipCell(g, d._stats) },
- ];
-
- for (let i = 0; i < maxTop; i++) {
- html += `<tr style="background:rgba(255,255,255,0.06);">
- <td colspan="${jobData.length+1}" style="font-weight:700;font-size:13px;color:#e3b341;padding:5px 8px;">
- \ud83c\udfc6 Top ${i+1} \uc0ac\ud6c4\ud569\uaca9 \ud6c4\ubcf4
- </td>
- </tr>`;
-
- for (const m of metricDefs) {
- html += `<tr><td class="text-end" style="color:#8b949e;padding:2px 8px;">${m.label}</td>`;
- for (const d of jobData) {
- const g = (d._gatedList || [])[i];
- html += g
- ? `<td class="${m.label.includes('파라미터') ? 'text-start' : 'text-end'}" style="padding:4px 6px; vertical-align:middle;">${m.fn(g, d)}</td>`
- : `<td class="text-muted text-center" style="padding:2px 6px;">\u2014</td>`;
- }
- html += '</tr>';
- }
- }
-
- html += '</tbody></table>';
- document.getElementById('optCmpTableWrap').innerHTML = html;
-
- } catch (err) {
- document.getElementById('optCmpTableWrap').innerHTML =
- `<div class="text-danger py-3 text-center">\uc624\ub958: ${err}</div>`;
- } finally {
- document.getElementById('optCmpLoading').classList.add('d-none');
- }
-}
--- a/backtest_web.py
+++ b/backtest_web.py
@@ -1907,8 +1907,6 @@
db.close()
-_feed_stats_cache = {}
-
@app.route("/api/feed_collect_stats", methods=["GET"])
def api_feed_collect_stats():
"""일자별 틱·호가·filter_eval 수집량 (벤더별). 통계 탭.
@@ -1915,7 +1915,6 @@
기본=COUNT + 폴백나이. heavy=1 이면 종목교집합·filter_eval 합격/거부 추가. 최근 N일 일괄집계 없음.
"""
from kis_trader.web.feed_collect_stats import build_feed_collect_stats
- import time
day = (request.args.get("date") or request.args.get("day") or "").strip()[:10]
if not day:
@@ -1922,20 +1922,8 @@
heavy_raw = (request.args.get("heavy") or request.args.get("detail") or "0").strip().lower()
heavy = heavy_raw in ("1", "true", "yes", "y", "on")
-
- cache_key = f"{day}_{heavy}"
- now = time.time()
-
- # 캐시 확인 (오늘 데이터는 60초 갱신, 과거 데이터는 영구 캐시)
- if cache_key in _feed_stats_cache:
- cached = _feed_stats_cache[cache_key]
- is_today = (day.replace("-", "") == _default_trading_day_ymd().replace("-", ""))
- if not is_today or (now - cached["ts"] < 60):
- return jsonify({"ok": True, **cached["data"]})
-
db = _db()
try:
payload = build_feed_collect_stats(db, day=day, heavy=heavy)
- _feed_stats_cache[cache_key] = {"ts": now, "data": payload}
return jsonify({"ok": True, **payload})
except Exception as e:
logger.exception("feed_collect_stats 실패")
@@ -9719,30 +9719,6 @@
return jsonify({"ok": True, "job": owj.refresh_job_status(meta)})
-@app.route("/api/optuna/topn/<job_id>", methods=["GET"])
-def api_optuna_topn(job_id: str):
- from kis_trader.backtest import optuna_web_jobs as owj
- meta = owj.load_job(job_id)
- if not meta:
- return jsonify({"ok": False, "error": "없는 job_id"}), 404
- rs = meta.get("result_summary") or {}
- return jsonify({
- "ok": True,
- "job_id": job_id,
- "label": meta.get("label") or meta.get("strategy") or job_id,
- "strategy": meta.get("strategy"),
- "sort_by": meta.get("sort_by"),
- "mode": meta.get("mode"),
- "start": meta.get("start"),
- "end": meta.get("end"),
- "study_short": meta.get("study_short"),
- "tpe_includes_orderbook": rs.get("tpe_includes_orderbook"),
- "period_info": meta.get("period_info"),
- "top5_gated": rs.get("top5_gated") or [],
- "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