From b74f305654406d558a9fd935d1b19a6c95b03064 Mon Sep 17 00:00:00 2001 From: Macbook Date: Thu, 11 Jun 2026 16:00:20 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B0=B1=ED=85=8C=EC=8A=A4=ED=8C=85=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=83=81=EB=8B=A8=20=ED=88=B4=EB=B0=94?= =?UTF-8?q?=EC=97=90=20=EB=B9=A0=EB=A5=B8=20=EC=8B=A4=ED=96=89=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 헤더 중앙에 종목·타임프레임·전략 드롭다운 + 실행 버튼 추가 - 실행 클릭 시 /api/candles/history에서 캔들 로드 → runBacktest 호출 - 완료 후 결과 목록 자동 갱신 및 백테스팅 탭 활성화 - 실행 중 스피너 표시, 오류 시 인라인 에러 메시지 표시 - btd-header-run·btd-run-select·btd-run-btn·btd-run-spinner CSS 추가 Co-authored-by: Cursor --- .../src/components/BacktestHistoryPage.tsx | 129 +++++++++++++++++- frontend/src/styles/backtestDashboard.css | 87 ++++++++++++ 2 files changed, 214 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/BacktestHistoryPage.tsx b/frontend/src/components/BacktestHistoryPage.tsx index 664b0d9..3b0e288 100644 --- a/frontend/src/components/BacktestHistoryPage.tsx +++ b/frontend/src/components/BacktestHistoryPage.tsx @@ -9,12 +9,17 @@ import { loadPaperTrades, loadPaperSummary, loadStrategies, + runBacktest, + loadBacktestSettings, + API_BASE, type BacktestResultRecord, type BacktestAnalysis, type BacktestSignal, type StrategyDto, } from '../utils/backendApi'; -import type { Theme } from '../types'; +import { UPBIT_MARKETS } from '../utils/upbitApi'; +import { timeframeToCandleType } from '../utils/chartCandleType'; +import type { Theme, Timeframe } from '../types'; import { buildEquityFromSignals } from '../utils/backtestEquity'; import { formatChartTypeKo, @@ -67,6 +72,19 @@ interface Props { theme?: Theme; } +const QUICK_RUN_TIMEFRAMES: { value: Timeframe; label: string }[] = [ + { value: '1m', label: '1분' }, + { value: '3m', label: '3분' }, + { value: '5m', label: '5분' }, + { value: '10m', label: '10분' }, + { value: '15m', label: '15분' }, + { value: '30m', label: '30분' }, + { value: '1h', label: '1시간' }, + { value: '4h', label: '4시간' }, + { value: '1D', label: '일봉' }, + { value: '1W', label: '주봉' }, +]; + export function BacktestHistoryPage({ theme = 'dark' }: Props) { const [tab, setTab] = useState('backtest'); const [records, setRecords] = useState([]); @@ -77,6 +95,13 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) { const [reportOpen, setReportOpen] = useState(false); const [strategies, setStrategies] = useState([]); + // ── 빠른 백테스팅 실행 ────────────────────────────────────────────────────── + const [runMarket, setRunMarket] = useState(UPBIT_MARKETS[0]); + const [runTimeframe, setRunTimeframe] = useState('1D'); + const [runStrategyId, setRunStrategyId] = useState(null); + const [runLoading, setRunLoading] = useState(false); + const [runError, setRunError] = useState(null); + const [leftWidth, setLeftWidth] = useState(() => readMinWidth(LEFT_KEY, LEFT_DEFAULT)); const [rightWidth, setRightWidth] = useState(() => readMinWidth(RIGHT_KEY, RIGHT_DEFAULT)); const leftRef = useRef(leftWidth); @@ -173,6 +198,49 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) { await fetchAll(); }, [records, fetchAll]); + const handleQuickRun = useCallback(async () => { + if (!runStrategyId) { + window.alert('전략을 선택해주세요.'); + return; + } + setRunLoading(true); + setRunError(null); + try { + const [settings, barsRes] = await Promise.all([ + loadBacktestSettings(), + fetch(`${API_BASE}/candles/history?${new URLSearchParams({ + market: runMarket, + type: timeframeToCandleType(runTimeframe), + count: '800', + })}`), + ]); + if (!barsRes.ok) throw new Error(`캔들 로딩 실패 (${barsRes.status})`); + const bars = (await barsRes.json()) as Array<{ + time: number; open: number; high: number; low: number; close: number; volume: number; + }>; + if (bars.length < 5) { + throw new Error('캔들 데이터가 부족합니다. 잠시 후 다시 시도하거나 타임프레임을 변경해 주세요.'); + } + const strategy = strategies.find(s => s.id === runStrategyId); + const result = await runBacktest({ + strategyId: runStrategyId, + bars, + timeframe: runTimeframe, + settings, + symbol: runMarket, + strategyName: strategy?.name, + }); + if (!result) throw new Error('백테스팅 실행에 실패했습니다.'); + // 결과 목록 갱신 후 새 결과 자동 선택 + await fetchAll(); + setTab('backtest'); + } catch (e) { + setRunError(e instanceof Error ? e.message : '실행 중 오류가 발생했습니다.'); + } finally { + setRunLoading(false); + } + }, [runMarket, runTimeframe, runStrategyId, strategies, fetchAll]); + const backtestDetail = selectedBacktest ? parseDetail(selectedBacktest) : null; const activeSignals = useMemo((): BacktestSignal[] => { @@ -289,7 +357,7 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) { Golden Analysis Command Center -
분석 데이터 로딩…
+
분석 데이터 로딩 중…
); } @@ -301,6 +369,63 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {

백테스팅

Golden Analysis Command Center + + {/* 빠른 실행 툴바 */} +
+ + + + + {runError && ( + ⚠ {runError} + )} +
+
{tab === 'backtest' && ( diff --git a/frontend/src/styles/backtestDashboard.css b/frontend/src/styles/backtestDashboard.css index f487bd7..8b9b5f8 100644 --- a/frontend/src/styles/backtestDashboard.css +++ b/frontend/src/styles/backtestDashboard.css @@ -838,6 +838,93 @@ gap: 8px; } +/* ── 빠른 백테스팅 실행 툴바 ────────────────────────────────────────────── */ +.btd-header-run { + display: flex; + align-items: center; + gap: 6px; + flex: 1; + justify-content: center; + min-width: 0; +} + +.btd-run-select { + background: var(--se-bg-elevated, var(--bg2)); + border: 1px solid var(--se-border); + color: var(--se-text); + border-radius: 7px; + padding: 5px 8px; + font-size: 0.76rem; + cursor: pointer; + height: 30px; + outline: none; + transition: border-color 0.15s; + min-width: 0; +} +.btd-run-select:focus { + border-color: var(--se-accent, #3d9be9); +} +.btd-run-select:disabled { + opacity: 0.45; + cursor: not-allowed; +} +.btd-run-select--strategy { + flex: 1; + max-width: 180px; +} + +.btd-run-btn { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border-radius: 50%; + border: 1px solid var(--se-accent, #3d9be9); + background: color-mix(in srgb, var(--se-accent, #3d9be9) 18%, transparent); + color: var(--se-accent, #3d9be9); + cursor: pointer; + transition: background 0.15s, border-color 0.15s, transform 0.1s; + flex-shrink: 0; +} +.btd-run-btn:hover:not(:disabled) { + background: color-mix(in srgb, var(--se-accent, #3d9be9) 32%, transparent); + transform: scale(1.08); +} +.btd-run-btn:active:not(:disabled) { + transform: scale(0.95); +} +.btd-run-btn:disabled { + opacity: 0.35; + cursor: not-allowed; +} +.btd-run-btn--loading { + border-color: var(--se-text-muted); + color: var(--se-text-muted); +} + +.btd-run-spinner { + display: inline-block; + width: 12px; + height: 12px; + border: 2px solid currentColor; + border-top-color: transparent; + border-radius: 50%; + animation: btd-spin 0.7s linear infinite; +} +@keyframes btd-spin { + to { transform: rotate(360deg); } +} + +.btd-run-error { + font-size: 0.71rem; + color: var(--se-sell, #f23645); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 200px; +} + .btd-btn { border: 1px solid var(--se-border); background: var(--se-bg-elevated, var(--bg2));