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

@@ -4166,11 +4166,12 @@
<button class="btn btn-sm btn-outline-secondary" onclick="optunaRefreshJobs()">새로고침</button>
</div>
</div>
<small class="text-muted d-block mt-2">승리식 기본=수익·낙폭·표본 · (구)=PnL/max(MDD,하한) · min_trades: 꼬리=1 고정 · 그 외=거래일×2(기본) · 탐색 WR/PF=0 · 사후=results_gated · DB 자동적용 안 함 · 2개 이상=순차 · 이력소스=후보 테이블 · 꼬리: align/limit_atr · 돌파: fixed/atr×호가 off/on</small>
<small class="text-muted d-block mt-2">전략 1개만=1·2차 연쇄 · 2개 이상·꼬리2모드·돌파多=순차(조합마다 1·2차) · 승리식=수익·낙폭·표본 · min_trades: 꼬리=1 · 그 외=거래일×2 · WR/PF=0 · DB 미적용</small>
</div>
<div class="card p-3 mb-3" id="opt_status_card">
<h6>진행</h6>
<div id="opt_period_banner" class="opt-period-banner d-none mb-2 p-2 rounded small"></div>
<div class="small text-muted mb-1">학습 trial</div>
<div class="optuna-prog-track mb-2" style="height:10px">
<div class="optuna-prog-fill" id="opt_tab_fill" style="width:0%"></div>
@@ -4187,6 +4188,7 @@
</div>
<div><span class="text-muted"></span> <code id="opt_st_job"></code></div>
<div><span class="text-muted">study</span> <code id="opt_st_study" style="font-size:11px"></code></div>
<div id="opt_st_refine_wrap" class="d-none"><span class="text-muted">1·2차</span> <span id="opt_st_refine" class="small" style="font-size:11px"></span></div>
<div><span class="text-muted">trial</span> <span id="opt_st_trials"></span></div>
</div>
<div class="col-md-6">
@@ -4213,6 +4215,28 @@
</div>
</div>
<div class="card p-3 mb-3" id="opt_gated_top3_card">
<h6 id="opt_gated_top3_title">사후합격 Top5 <small class="text-muted">DB 적용 후보 · 일평균(원/%) · 익절/손절 비교</small></h6>
<div id="opt_gated_top3_body" class="small text-muted">완료 후 표시</div>
</div>
<div class="card p-3 mb-3" id="opt_param_dist_card">
<h6>사후합격 pool 파라미터 분포 <small class="text-muted">분포 참고용 · DB 적용 1순위 아님</small></h6>
<div id="opt_param_dist_legend" class="small mb-2 p-2 rounded" style="background:rgba(56,139,253,0.06);border:1px solid var(--border);font-size:11px;line-height:1.6">
<div class="fw-semibold mb-1">범례 (막대 1줄 = 파라미터 1개)</div>
<div class="d-flex flex-wrap gap-3 align-items-center">
<span><span style="display:inline-block;width:28px;height:10px;background:rgba(56,139,253,0.45);border-radius:2px;vertical-align:middle"></span>
<b class="ms-1">파란 띠</b> = p25~p75 · 사후합격 후보들의 <b>중간 50% 구간</b> (너무 극단값 제외한 흔한 범위)</span>
<span><span style="display:inline-block;width:2px;height:12px;background:#3fb950;vertical-align:middle"></span>
<b class="ms-1">녹색 세로줄</b> = median · <b>중앙값</b> (위·아래 절반이 이 값 기준)</span>
<span><span style="display:inline-block;width:8px;height:8px;background:#f0883e;border-radius:50%;vertical-align:middle"></span>
<b class="ms-1">주황 동그라미</b> = mode · <b>최빈값</b> (후보 중 가장 많이 나온 숫자 · 괄호 %는 비율)</span>
</div>
<div class="text-muted mt-1">아래 숫자(p25·med·p75)는 막대와 동일. 익절·손절이 Top5마다 다르게 보이면 이 밴드가 넓다는 뜻입니다.</div>
</div>
<div id="opt_param_dist_chart" class="small text-muted">완료 후 표시</div>
</div>
<div class="card p-3 mb-3">
<div id="opt_overfit_box" class="p-2 small" style="background:rgba(248,81,73,0.06);border:1px solid var(--border);border-radius:6px">
<div class="d-flex flex-wrap justify-content-between gap-2 mb-1">
@@ -4272,7 +4296,8 @@
<tr>
<th>#</th><th>trial</th><th class="text-end">거래</th>
<th class="text-end">승률</th><th class="text-end">PF</th><th class="text-end">PnL</th>
<th class="text-end" title="총손익 ÷ 기간 한국거래일수. 짧은/긴 구간 비교용.">일평균</th>
<th class="text-end" title="총손익 ÷ 기간 한국거래일수. 짧은/긴 구간 비교용.">일평균(원)</th>
<th class="text-end" title="총수익률(운용한도) ÷ 기간 거래일수">일평균%</th>
<th title="본 TPE: 호가ON/OFF·spr/r/ask · 익절%/상한 · 손절% · 휩쏘ON/OFF·수치 (사후8방 아님)">호가·익절·손절·휩쏘</th>
<th class="text-end">손실일</th><th class="text-end">최악일</th>
<th class="text-end" title="높을수록 좋음. 만점 없음. 일평균PnL λ×일표준편차(원).">안정점수 ↑</th>
@@ -4281,7 +4306,7 @@
</tr>
</thead>
<tbody id="opt_top5_learn_tbody">
<tr><td colspan="13" class="text-muted">완료 후 표시</td></tr>
<tr><td colspan="14" class="text-muted">완료 후 표시</td></tr>
</tbody>
</table>
</div>
@@ -4295,7 +4320,8 @@
<tr>
<th>#</th><th>trial</th><th class="text-end">거래</th>
<th class="text-end">승률</th><th class="text-end">PF</th><th class="text-end">PnL</th>
<th class="text-end" title="총손익 ÷ 기간 한국거래일수. 짧은/긴 구간 비교용.">일평균</th>
<th class="text-end" title="총손익 ÷ 기간 한국거래일수. 짧은/긴 구간 비교용.">일평균(원)</th>
<th class="text-end" title="총수익률(운용한도) ÷ 기간 거래일수">일평균%</th>
<th title="본 TPE: 호가ON/OFF·spr/r/ask · 익절%/상한 · 손절% · 휩쏘ON/OFF·수치 (사후8방 아님)">호가·익절·손절·휩쏘</th>
<th class="text-end">손실일</th><th class="text-end">최악일</th>
<th class="text-end" title="높을수록 좋음. 만점 없음. 일평균PnL λ×일표준편차(원).">안정점수 ↑</th>
@@ -4304,14 +4330,14 @@
</tr>
</thead>
<tbody id="opt_top5_tbody">
<tr><td colspan="13" class="text-muted">완료 후 표시</td></tr>
<tr><td colspan="14" class="text-muted">완료 후 표시</td></tr>
</tbody>
</table>
</div>
</div>
<div class="card p-3 mb-3">
<h6>mode_combo <small class="text-muted">축별 최빈 조립 · trial 번호 없음 · 탐색 TopN 합의 <b>1회 실측</b> — 표 컬럼=사후합격 Top10과 동일</small></h6>
<h6>mode_combo <small class="text-muted">축별 최빈 조립 · PnL 양수 pool · trial 번호 없음 · 2차 완료 후 표시</small></h6>
<div id="opt_mode_combo_body" class="small mb-2">완료 후 표시</div>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0" style="font-size:12px">
@@ -4319,7 +4345,8 @@
<tr>
<th>#</th><th>trial</th><th class="text-end">거래</th>
<th class="text-end">승률</th><th class="text-end">PF</th><th class="text-end">PnL</th>
<th class="text-end" title="총손익 ÷ 기간 한국거래일수. 짧은/긴 구간 비교용.">일평균</th>
<th class="text-end" title="총손익 ÷ 기간 한국거래일수. 짧은/긴 구간 비교용.">일평균(원)</th>
<th class="text-end" title="총수익률(운용한도) ÷ 기간 거래일수">일평균%</th>
<th title="본 TPE: 호가ON/OFF·spr/r/ask · 익절%/상한 · 손절% · 휩쏘ON/OFF·수치 (사후8방 아님)">호가·익절·손절·휩쏘</th>
<th class="text-end">손실일</th><th class="text-end">최악일</th>
<th class="text-end" title="높을수록 좋음. 만점 없음. 일평균PnL λ×일표준편차(원).">안정점수 ↑</th>
@@ -4328,7 +4355,33 @@
</tr>
</thead>
<tbody id="opt_top5_mode_tbody">
<tr><td colspan="13" class="text-muted">완료 후 표시 (mode 실측 1행 · 사후합격과 같은 열)</td></tr>
<tr><td colspan="14" class="text-muted">완료 후 표시 (mode 실측 1행 · 사후합격과 같은 열)</td></tr>
</tbody>
</table>
</div>
</div>
<div class="card p-3 mb-3">
<h6>mode Top10 <small class="text-muted">results_mode · PnL 양수 pool p25~p75 밴드 <b>근접도</b> ↑ · trial 있음 · <b>적용 1순위 아님</b></small></h6>
<div id="opt_mode_band_top3" class="small mb-2 text-muted"></div>
<div id="opt_mode_top_meta" class="small text-muted mb-1"></div>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0" style="font-size:12px">
<thead>
<tr>
<th>#</th><th>trial</th><th class="text-end" title="Top-N pool 축별 p25~p75 밴드 안/근접 — 100에 가까울수록 흔한 구간">근접% ↑</th><th class="text-end">거래</th>
<th class="text-end">승률</th><th class="text-end">PF</th><th class="text-end">PnL</th>
<th class="text-end" title="총손익 ÷ 기간 한국거래일수">일평균(원)</th>
<th class="text-end" title="총수익률(운용한도) ÷ 기간 거래일수">일평균%</th>
<th title="본 TPE trial 호가·익절·손절·휩쏘">호가·익절·손절·휩쏘</th>
<th class="text-end">손실일</th><th class="text-end">최악일</th>
<th class="text-end" title="일평균PnL λ×일표준편차">안정점수 ↑</th>
<th class="text-end" title="과적합 추정 0~100">과적합% ↓</th>
<th></th>
</tr>
</thead>
<tbody id="opt_top5_mode_top_tbody">
<tr><td colspan="15" class="text-muted">완료 후 표시 (mode Top10 · 구 JSON은 웹에서 즉시 재계산)</td></tr>
</tbody>
</table>
</div>
@@ -4343,7 +4396,8 @@
<tr>
<th>#</th><th>trial</th><th class="text-end">거래</th>
<th class="text-end">승률</th><th class="text-end">PF</th><th class="text-end">PnL</th>
<th class="text-end" title="총손익 ÷ 기간 한국거래일수. 짧은/긴 구간 비교용.">일평균</th>
<th class="text-end" title="총손익 ÷ 기간 한국거래일수. 짧은/긴 구간 비교용.">일평균(원)</th>
<th class="text-end" title="총수익률(운용한도) ÷ 기간 거래일수">일평균%</th>
<th title="본 TPE: 호가ON/OFF·spr/r/ask · 익절%/상한 · 손절% · 휩쏘ON/OFF·수치 (사후8방 아님)">호가·익절·손절·휩쏘</th>
<th class="text-end">손실일</th><th class="text-end">최악일</th>
<th class="text-end" title="높을수록 좋음. 만점 없음. 일평균PnL λ×일표준편차(원).">안정점수 ↑</th>
@@ -4352,24 +4406,10 @@
</tr>
</thead>
<tbody id="opt_top5_stable_tbody">
<tr><td colspan="13" class="text-muted">완료 후 표시 (구 JSON은 재실행 필요)</td></tr>
<tr><td colspan="14" class="text-muted">완료 후 표시 (구 JSON은 재실행 필요)</td></tr>
</tbody>
</table>
</div>
<div id="opt_postprocess_rec" class="mt-3 p-2 small" style="background:rgba(63,185,80,0.06);border:1px solid var(--border);border-radius:6px">
<div class="text-muted mb-1">후처리 비교표
<span class="text-muted">(사후합격「상세」· gated 없으면 학습Top learn# 폴백 · mode/live)</span>
</div>
<div class="mb-2">
<button class="btn btn-sm btn-warning py-0" id="opt_btn_pp_rerun" onclick="optunaRerunPostprocess()">이 잡 후처리 재실행</button>
<button class="btn btn-sm btn-outline-secondary py-0 ms-1" id="opt_btn_mode_detail" onclick="optunaSelectPostprocess('mode',1)">mode 상세</button>
<span id="opt_pp_rerun_status" class="text-muted ms-1">끝난 잡은 이 버튼이 켜집니다. 8방이 비면 누르세요(호가스냅 부족이면 재실행해도 동일).</span>
</div>
<div id="opt_postprocess_consensus" class="mb-1">합의: 완료 후 표시</div>
<div id="opt_postprocess_cards" style="max-height:min(72vh,680px);overflow:auto;padding-right:4px">
<div class="text-muted">사후합격 또는 안정 Top에서 「상세」를 누르면 이 표가 바뀝니다.</div>
</div>
</div>
</div>
<div class="card p-3 mb-3" id="opt_detail_card">
@@ -4412,24 +4452,30 @@
<div id="opt_join_overlay" class="opt-join-overlay" onclick="if(event.target===this)optunaCloseJoinCmd()">
<div class="opt-join-panel" role="dialog" aria-labelledby="opt_join_title">
<div class="d-flex justify-content-between align-items-center gap-2 mb-2">
<h6 class="mb-0" id="opt_join_title">다른 PC에서 같은 study 붙이기</h6>
<h6 class="mb-0" id="opt_join_title">다른 PC 병렬 · study 이어 붙이기</h6>
<button type="button" class="btn btn-sm btn-outline-secondary py-0" onclick="optunaCloseJoinCmd()">닫기</button>
</div>
<p id="opt_join_hint" class="small text-muted mb-2" style="white-space:pre-wrap"></p>
<div class="small text-muted">지금 study</div>
<div id="opt_join_refine" class="mb-3"></div>
<div class="small text-muted">지금 study (trial 추가용)</div>
<code id="opt_join_study" style="font-size:11px"></code>
<div class="d-flex justify-content-between align-items-center mt-2 mb-1">
<span class="small text-muted">Linux / VM (bash)</span>
<span class="small text-muted">Linux / VM (bash) — 같은 study trial 추가</span>
<button type="button" class="btn btn-sm btn-outline-secondary py-0" onclick="optunaCopyJoin('join')">복사</button>
</div>
<pre id="opt_join_cmd" class="opt-join-pre mb-2"></pre>
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="small text-muted">Windows PowerShell (이걸 복사)</span>
<span class="small text-muted">Windows PowerShell — trial 추가</span>
<button type="button" class="btn btn-sm btn-primary py-0" onclick="optunaCopyJoin('ps')">복사</button>
</div>
<pre id="opt_join_cmd_ps" class="opt-join-pre mb-2"></pre>
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="small text-muted">웹이 실행한 명령 (순차=bash · 그대로 돌리면 새 study)</span>
<span class="small text-muted">순차 전체 (env 포함 bash · 이 VM과 동일)</span>
<button type="button" class="btn btn-sm btn-outline-secondary py-0" onclick="optunaCopyJoin('web_full')">복사</button>
</div>
<pre id="opt_web_cmd_full" class="opt-join-pre mb-2"></pre>
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="small text-muted">웹 실행 요약 (참고)</span>
<button type="button" class="btn btn-sm btn-outline-secondary py-0" onclick="optunaCopyJoin('web')">복사</button>
</div>
<pre id="opt_web_cmd" class="opt-join-pre mb-2"></pre>
@@ -4437,6 +4483,6 @@
</div>
</div>
<script src="{{ url_for('static', filename='js/backtest.js') }}?v=20260823obDbTruth"></script>
<script src="{{ url_for('static', filename='js/backtest.js') }}?v=20260830opt_p2"></script>
</body>
</html>