diff --git a/src/api.js b/src/api.js
index a85affd..02ba9bf 100644
--- a/src/api.js
+++ b/src/api.js
@@ -740,6 +740,11 @@ export async function triggerEvolverEvaluate() {
return r.json();
}
+// --- Lotto Backtest ---
+export const lottoBacktestTrackRecord = () => apiGet('/api/lotto/backtest/track-record');
+export const lottoBacktestCalibration = (weeks=52) => apiGet(`/api/lotto/backtest/calibration?weeks=${weeks}`);
+export const lottoBacktestReview = (drawNo) => apiGet(`/api/lotto/backtest/review/${drawNo}`);
+
// --- Tarot Lab ---
export function tarotInterpret(body) {
diff --git a/src/pages/lotto/Evolver.css b/src/pages/lotto/Evolver.css
index fed3069..2fec4ff 100644
--- a/src/pages/lotto/Evolver.css
+++ b/src/pages/lotto/Evolver.css
@@ -58,6 +58,9 @@
.winner-card .winner-meta strong { color: #f1f5f9; font-weight: 600; }
.winner-card .winner-chart { background: rgba(0,0,0,0.15); border-radius: 8px; padding: 8px; }
+/* Backtest — WinnerAnalysisCard chart wrapper (standalone, not inside .winner-card) */
+.backtest-winner-chart { background: rgba(0,0,0,0.15); border-radius: 8px; padding: 8px; }
+
/* TrialsGrid */
.trials-grid .grid {
display: grid; grid-template-columns: repeat(6, 1fr);
@@ -186,6 +189,47 @@
font-size: 0.75rem;
}
+/* Backtest — TrackRecordCard */
+.backtest-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.85rem;
+ margin-bottom: 10px;
+}
+.backtest-table th {
+ text-align: left;
+ color: #94a3b8;
+ font-weight: 500;
+ padding: 6px 8px;
+ border-bottom: 1px solid rgba(255,255,255,0.08);
+}
+.backtest-table td {
+ padding: 6px 8px;
+ color: #cbd5e1;
+ border-bottom: 1px solid rgba(255,255,255,0.04);
+}
+.backtest-table tr:last-child td { border-bottom: none; }
+
+/* Backtest — shared note */
+.backtest-note {
+ margin: 8px 0 0;
+ color: #64748b;
+ font-size: 0.8rem;
+ line-height: 1.4;
+}
+.backtest-note strong { color: #cbd5e1; }
+
+/* Backtest — section divider */
+.backtest-section-header {
+ margin: 8px 0 4px;
+ color: #94a3b8;
+ font-size: 0.85rem;
+ font-weight: 600;
+ letter-spacing: 0.03em;
+ border-top: 1px solid rgba(255,255,255,0.06);
+ padding-top: 14px;
+}
+
@media (max-width: 640px) {
.trials-grid .grid { grid-template-columns: repeat(3, 1fr); height: auto; }
.base-diff .diff-grid { grid-template-columns: repeat(3, 1fr); }
diff --git a/src/pages/lotto/evolver/CalibrationChart.jsx b/src/pages/lotto/evolver/CalibrationChart.jsx
new file mode 100644
index 0000000..52d79f2
--- /dev/null
+++ b/src/pages/lotto/evolver/CalibrationChart.jsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import {
+ LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
+} from 'recharts';
+
+export default function CalibrationChart({ history }) {
+ if (!history || history.length === 0) {
+ return (
+
+
당첨조합 캘리브레이션 추세
+
캘리브레이션 데이터가 없습니다.
+
+ );
+ }
+
+ // history는 DESC 순서로 오므로 역순해서 오름차순 x축
+ const data = [...history].reverse().map((h) => ({
+ draw: h.draw_no,
+ score: h.score_total != null ? +h.score_total.toFixed(3) : null,
+ pct: h.percentile != null ? +h.percentile.toFixed(3) : null,
+ }));
+
+ return (
+
+
당첨조합 캘리브레이션 추세 (최근 {history.length}회차)
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/lotto/evolver/TrackRecordCard.jsx b/src/pages/lotto/evolver/TrackRecordCard.jsx
new file mode 100644
index 0000000..72600c7
--- /dev/null
+++ b/src/pages/lotto/evolver/TrackRecordCard.jsx
@@ -0,0 +1,56 @@
+import React from 'react';
+
+const STRATEGY_ORDER = ['engine_w', 'random_null', 'coverage'];
+const STRATEGY_LABEL = { engine_w: '엔진', random_null: '무작위', coverage: '커버리지' };
+
+export default function TrackRecordCard({ byStrategy }) {
+ if (!byStrategy) return null;
+
+ const rows = STRATEGY_ORDER.filter((s) => byStrategy[s]);
+
+ return (
+
+
누적 성적표
+ {rows.length === 0 ? (
+
아직 백테스트 데이터가 없습니다.
+ ) : (
+ <>
+
+
+
+ | 전략 |
+ 누적 장수 |
+ 회차수 |
+ 1등 |
+ 2등 |
+ 3등 |
+ 4등 |
+ 5등 |
+
+
+
+ {rows.map((s) => {
+ const a = byStrategy[s];
+ return (
+
+ | {STRATEGY_LABEL[s] || s} |
+ {(a.n_tickets || 0).toLocaleString()} |
+ {a.draws || 0} |
+ {a['1st'] || 0} |
+ {a['2nd'] || 0} |
+ {a['3rd'] || 0} |
+ {a['4th'] || 0} |
+ {a['5th'] || 0} |
+
+ );
+ })}
+
+
+
+ 엔진이 무작위를 넘지 못하면 분석에 통계적 우위가 없다는 정직한 증거입니다.
+
+ >
+ )}
+
+ );
+}
diff --git a/src/pages/lotto/evolver/WinnerAnalysisCard.jsx b/src/pages/lotto/evolver/WinnerAnalysisCard.jsx
new file mode 100644
index 0000000..af860b3
--- /dev/null
+++ b/src/pages/lotto/evolver/WinnerAnalysisCard.jsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import {
+ RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis,
+ Radar, ResponsiveContainer,
+} from 'recharts';
+
+export default function WinnerAnalysisCard({ analysis }) {
+ if (!analysis) return null;
+
+ const data = [
+ { k: '빈도', v: analysis.score_frequency ?? 0 },
+ { k: '지문', v: analysis.score_fingerprint ?? 0 },
+ { k: '갭', v: analysis.score_gap ?? 0 },
+ { k: '공동출현', v: analysis.score_cooccur ?? 0 },
+ { k: '다양성', v: analysis.score_diversity ?? 0 },
+ ];
+
+ const pct = analysis.percentile != null
+ ? `${(analysis.percentile * 100).toFixed(0)}%`
+ : '—';
+
+ return (
+
+
+ 이번 당첨조합 분석치
+ 무작위 상위 {pct}
+
+
+
+ 종합 점수: {(analysis.score_total ?? 0).toFixed(3)}
+
+
+ );
+}
diff --git a/src/pages/lotto/tabs/EvolverTab.jsx b/src/pages/lotto/tabs/EvolverTab.jsx
index 4699268..6d54418 100644
--- a/src/pages/lotto/tabs/EvolverTab.jsx
+++ b/src/pages/lotto/tabs/EvolverTab.jsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import '../Evolver.css';
import { useEvolverApi } from '../evolver/useEvolverApi';
import WinnerCard from '../evolver/WinnerCard';
@@ -7,10 +7,40 @@ import BaseDiff from '../evolver/BaseDiff';
import BaseHistory from '../evolver/BaseHistory';
import LottoActivityTimeline from '../evolver/LottoActivityTimeline';
import EvolverActions from '../evolver/EvolverActions';
+import TrackRecordCard from '../evolver/TrackRecordCard';
+import CalibrationChart from '../evolver/CalibrationChart';
+import WinnerAnalysisCard from '../evolver/WinnerAnalysisCard';
+import { getLatest, lottoBacktestTrackRecord, lottoBacktestCalibration, lottoBacktestReview } from '../../../api';
export default function EvolverTab() {
const { status, history, activity, loading, error, refetch } = useEvolverApi({ days: 7, weeks: 12 });
+ const [trackRecord, setTrackRecord] = useState(null);
+ const [calibHistory, setCalibHistory] = useState([]);
+ const [winnerAnalysis, setWinnerAnalysis] = useState(null);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ const [tr, cal] = await Promise.all([
+ lottoBacktestTrackRecord(),
+ lottoBacktestCalibration(52),
+ ]);
+ setTrackRecord(tr);
+ setCalibHistory(cal.history || []);
+ } catch (_) { /* 백엔드 미준비 시 graceful skip */ }
+
+ try {
+ const latest = await getLatest();
+ const drawNo = latest?.drawNo || latest?.drw_no || latest?.draw_no;
+ if (drawNo) {
+ const review = await lottoBacktestReview(drawNo);
+ setWinnerAnalysis(review.winner_analysis || null);
+ }
+ } catch (_) { /* 아직 데이터 없으면 null 유지 */ }
+ })();
+ }, []);
+
if (loading) return ;
if (error) return ;
@@ -73,6 +103,16 @@ export default function EvolverTab() {
>
)}
+
+ {/* 백테스트 성적표 · 캘리브레이션 · 당첨조합 분석 */}
+ {(winnerAnalysis || trackRecord || calibHistory.length > 0) && (
+ <>
+ 백테스트 & 캘리브레이션
+
+
+
+ >
+ )}
);
}