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));