백테스팅 화면 상단 툴바에 빠른 실행 기능 추가

- 헤더 중앙에 종목·타임프레임·전략 드롭다운 + 실행 버튼 추가
- 실행 클릭 시 /api/candles/history에서 캔들 로드 → runBacktest 호출
- 완료 후 결과 목록 자동 갱신 및 백테스팅 탭 활성화
- 실행 중 스피너 표시, 오류 시 인라인 에러 메시지 표시
- btd-header-run·btd-run-select·btd-run-btn·btd-run-spinner CSS 추가

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Macbook
2026-06-11 16:00:20 +09:00
parent 33621883dc
commit b74f305654
2 changed files with 214 additions and 2 deletions
+127 -2
View File
@@ -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<ExecutionListTab>('backtest');
const [records, setRecords] = useState<BacktestResultRecord[]>([]);
@@ -77,6 +95,13 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
const [reportOpen, setReportOpen] = useState(false);
const [strategies, setStrategies] = useState<StrategyDto[]>([]);
// ── 빠른 백테스팅 실행 ──────────────────────────────────────────────────────
const [runMarket, setRunMarket] = useState<string>(UPBIT_MARKETS[0]);
const [runTimeframe, setRunTimeframe] = useState<Timeframe>('1D');
const [runStrategyId, setRunStrategyId] = useState<number | null>(null);
const [runLoading, setRunLoading] = useState(false);
const [runError, setRunError] = useState<string | null>(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) {
<span className="btd-header-sub">Golden Analysis Command Center</span>
</div>
</header>
<div className="btd-loading"> </div>
<div className="btd-loading"> </div>
</div>
);
}
@@ -301,6 +369,63 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
<h1 className="btd-header-title"></h1>
<span className="btd-header-sub">Golden Analysis Command Center</span>
</div>
{/* 빠른 실행 툴바 */}
<div className="btd-header-run">
<select
className="btd-run-select"
value={runMarket}
onChange={e => setRunMarket(e.target.value)}
disabled={runLoading}
title="종목"
>
{UPBIT_MARKETS.map(m => (
<option key={m} value={m}>{m.replace('KRW-', '')}</option>
))}
</select>
<select
className="btd-run-select"
value={runTimeframe}
onChange={e => setRunTimeframe(e.target.value as Timeframe)}
disabled={runLoading}
title="타임프레임"
>
{QUICK_RUN_TIMEFRAMES.map(tf => (
<option key={tf.value} value={tf.value}>{tf.label}</option>
))}
</select>
<select
className="btd-run-select btd-run-select--strategy"
value={runStrategyId ?? ''}
onChange={e => setRunStrategyId(e.target.value ? Number(e.target.value) : null)}
disabled={runLoading}
title="전략"
>
<option value=""> </option>
{strategies.map(s => (
<option key={s.id} value={s.id}>{s.name}</option>
))}
</select>
<button
type="button"
className={`btd-run-btn${runLoading ? ' btd-run-btn--loading' : ''}`}
disabled={!runStrategyId || runLoading}
onClick={() => void handleQuickRun()}
title="백테스팅 실행"
>
{runLoading ? (
<span className="btd-run-spinner" />
) : (
<svg viewBox="0 0 16 16" width="13" height="13" fill="currentColor" aria-hidden="true">
<path d="M3 2.5l10 5.5-10 5.5V2.5z" />
</svg>
)}
</button>
{runError && (
<span className="btd-run-error" title={runError}> {runError}</span>
)}
</div>
<div className="btd-header-actions">
<button type="button" className="btd-btn btd-btn--ghost" onClick={() => void fetchAll()}></button>
{tab === 'backtest' && (