merge: 로또 자율학습 탭 — 성적표·캘리브레이션·당첨조합 분석
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -740,6 +740,11 @@ export async function triggerEvolverEvaluate() {
|
|||||||
return r.json();
|
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 ---
|
// --- Tarot Lab ---
|
||||||
|
|
||||||
export function tarotInterpret(body) {
|
export function tarotInterpret(body) {
|
||||||
|
|||||||
@@ -58,6 +58,9 @@
|
|||||||
.winner-card .winner-meta strong { color: #f1f5f9; font-weight: 600; }
|
.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; }
|
.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 */
|
/* TrialsGrid */
|
||||||
.trials-grid .grid {
|
.trials-grid .grid {
|
||||||
display: grid; grid-template-columns: repeat(6, 1fr);
|
display: grid; grid-template-columns: repeat(6, 1fr);
|
||||||
@@ -186,6 +189,47 @@
|
|||||||
font-size: 0.75rem;
|
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) {
|
@media (max-width: 640px) {
|
||||||
.trials-grid .grid { grid-template-columns: repeat(3, 1fr); height: auto; }
|
.trials-grid .grid { grid-template-columns: repeat(3, 1fr); height: auto; }
|
||||||
.base-diff .diff-grid { grid-template-columns: repeat(3, 1fr); }
|
.base-diff .diff-grid { grid-template-columns: repeat(3, 1fr); }
|
||||||
|
|||||||
53
src/pages/lotto/evolver/CalibrationChart.jsx
Normal file
53
src/pages/lotto/evolver/CalibrationChart.jsx
Normal file
@@ -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 (
|
||||||
|
<div className="evolver-card backtest-calibration empty">
|
||||||
|
<h2>당첨조합 캘리브레이션 추세</h2>
|
||||||
|
<p className="muted">캘리브레이션 데이터가 없습니다.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div className="evolver-card backtest-calibration">
|
||||||
|
<h2>당첨조합 캘리브레이션 추세 (최근 {history.length}회차)</h2>
|
||||||
|
<ResponsiveContainer width="100%" height={240}>
|
||||||
|
<LineChart data={data}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.08)" />
|
||||||
|
<XAxis dataKey="draw" tick={{ fill: '#94a3b8', fontSize: 11 }} />
|
||||||
|
<YAxis domain={[0, 1]} tick={{ fill: '#94a3b8', fontSize: 11 }} />
|
||||||
|
<Tooltip contentStyle={{ background: '#0f172a', border: '1px solid rgba(255,255,255,0.1)', color: '#e2e8f0' }} />
|
||||||
|
<Legend wrapperStyle={{ color: '#94a3b8', fontSize: '0.8rem' }} />
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="score"
|
||||||
|
stroke="#f59e0b"
|
||||||
|
dot={false}
|
||||||
|
name="당첨조합 분석치"
|
||||||
|
connectNulls
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="pct"
|
||||||
|
stroke="#34d399"
|
||||||
|
dot={false}
|
||||||
|
name="무작위 percentile"
|
||||||
|
connectNulls
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
src/pages/lotto/evolver/TrackRecordCard.jsx
Normal file
56
src/pages/lotto/evolver/TrackRecordCard.jsx
Normal file
@@ -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 (
|
||||||
|
<div className="evolver-card backtest-track-record">
|
||||||
|
<h2>누적 성적표</h2>
|
||||||
|
{rows.length === 0 ? (
|
||||||
|
<p className="backtest-note">아직 백테스트 데이터가 없습니다.</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<table className="backtest-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>전략</th>
|
||||||
|
<th>누적 장수</th>
|
||||||
|
<th>회차수</th>
|
||||||
|
<th>1등</th>
|
||||||
|
<th>2등</th>
|
||||||
|
<th>3등</th>
|
||||||
|
<th>4등</th>
|
||||||
|
<th>5등</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows.map((s) => {
|
||||||
|
const a = byStrategy[s];
|
||||||
|
return (
|
||||||
|
<tr key={s}>
|
||||||
|
<td>{STRATEGY_LABEL[s] || s}</td>
|
||||||
|
<td>{(a.n_tickets || 0).toLocaleString()}</td>
|
||||||
|
<td>{a.draws || 0}</td>
|
||||||
|
<td>{a['1st'] || 0}</td>
|
||||||
|
<td>{a['2nd'] || 0}</td>
|
||||||
|
<td>{a['3rd'] || 0}</td>
|
||||||
|
<td>{a['4th'] || 0}</td>
|
||||||
|
<td>{a['5th'] || 0}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p className="backtest-note">
|
||||||
|
엔진이 무작위를 넘지 못하면 분석에 통계적 우위가 없다는 정직한 증거입니다.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
src/pages/lotto/evolver/WinnerAnalysisCard.jsx
Normal file
49
src/pages/lotto/evolver/WinnerAnalysisCard.jsx
Normal file
@@ -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 (
|
||||||
|
<div className="evolver-card backtest-winner-analysis">
|
||||||
|
<h2>
|
||||||
|
이번 당첨조합 분석치
|
||||||
|
<span className="badge">무작위 상위 {pct}</span>
|
||||||
|
</h2>
|
||||||
|
<div className="backtest-winner-chart">
|
||||||
|
<ResponsiveContainer width="100%" height={240}>
|
||||||
|
<RadarChart data={data}>
|
||||||
|
<PolarGrid stroke="rgba(255,255,255,0.12)" />
|
||||||
|
<PolarAngleAxis dataKey="k" tick={{ fill: '#cbd5e1', fontSize: 12 }} />
|
||||||
|
<PolarRadiusAxis angle={90} domain={[0, 1]} tick={{ fill: '#64748b', fontSize: 10 }} />
|
||||||
|
<Radar
|
||||||
|
name="분석치"
|
||||||
|
dataKey="v"
|
||||||
|
stroke="#60a5fa"
|
||||||
|
fill="#60a5fa"
|
||||||
|
fillOpacity={0.4}
|
||||||
|
/>
|
||||||
|
</RadarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
<p className="backtest-note">
|
||||||
|
종합 점수: <strong>{(analysis.score_total ?? 0).toFixed(3)}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import '../Evolver.css';
|
import '../Evolver.css';
|
||||||
import { useEvolverApi } from '../evolver/useEvolverApi';
|
import { useEvolverApi } from '../evolver/useEvolverApi';
|
||||||
import WinnerCard from '../evolver/WinnerCard';
|
import WinnerCard from '../evolver/WinnerCard';
|
||||||
@@ -7,10 +7,40 @@ import BaseDiff from '../evolver/BaseDiff';
|
|||||||
import BaseHistory from '../evolver/BaseHistory';
|
import BaseHistory from '../evolver/BaseHistory';
|
||||||
import LottoActivityTimeline from '../evolver/LottoActivityTimeline';
|
import LottoActivityTimeline from '../evolver/LottoActivityTimeline';
|
||||||
import EvolverActions from '../evolver/EvolverActions';
|
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() {
|
export default function EvolverTab() {
|
||||||
const { status, history, activity, loading, error, refetch } = useEvolverApi({ days: 7, weeks: 12 });
|
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 <div className="lotto-evolver"><p className="lotto-evolver-muted">로딩 중...</p></div>;
|
if (loading) return <div className="lotto-evolver"><p className="lotto-evolver-muted">로딩 중...</p></div>;
|
||||||
if (error) return <div className="lotto-evolver"><p className="lotto-evolver-muted">에러: {String(error)}</p></div>;
|
if (error) return <div className="lotto-evolver"><p className="lotto-evolver-muted">에러: {String(error)}</p></div>;
|
||||||
|
|
||||||
@@ -73,6 +103,16 @@ export default function EvolverTab() {
|
|||||||
<EvolverActions onChange={refetch} />
|
<EvolverActions onChange={refetch} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 백테스트 성적표 · 캘리브레이션 · 당첨조합 분석 */}
|
||||||
|
{(winnerAnalysis || trackRecord || calibHistory.length > 0) && (
|
||||||
|
<>
|
||||||
|
<p className="backtest-section-header">백테스트 & 캘리브레이션</p>
|
||||||
|
<WinnerAnalysisCard analysis={winnerAnalysis} />
|
||||||
|
<TrackRecordCard byStrategy={trackRecord?.by_strategy} />
|
||||||
|
<CalibrationChart history={calibHistory} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user