모의투자, 백테스팅 레이아웃 수정
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* BacktestHistoryPage — 첨부 이미지 스타일 (타임라인 이력 + 대시보드)
|
||||
* BacktestHistoryPage — 전략편집기와 동일한 3열 + 하단 패널 레이아웃
|
||||
*/
|
||||
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
@@ -9,9 +9,11 @@ import {
|
||||
type BacktestAnalysis,
|
||||
type BacktestSignal,
|
||||
} from '../utils/backendApi';
|
||||
import type { Theme } from '../types';
|
||||
import { buildEquityFromSignals } from '../utils/backtestEquity';
|
||||
import { BacktestResultDashboard } from './backtest/BacktestResultDashboard';
|
||||
import BacktestSparkline from './backtest/BacktestSparkline';
|
||||
import BuilderPageShell from './layout/BuilderPageShell';
|
||||
|
||||
const pct = (v: number) =>
|
||||
isFinite(v) && v !== 0 ? `${v >= 0 ? '+' : ''}${(v * 100).toFixed(1)}%` : '0.0%';
|
||||
@@ -64,7 +66,11 @@ function TimelineItem({ r, active, onClick }: TimelineItemProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export function BacktestHistoryPage() {
|
||||
interface Props {
|
||||
theme?: Theme;
|
||||
}
|
||||
|
||||
export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
||||
const [records, setRecords] = useState<BacktestResultRecord[]>([]);
|
||||
const [selected, setSelected] = useState<BacktestResultRecord | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -89,59 +95,124 @@ export function BacktestHistoryPage() {
|
||||
|
||||
const selectedDetail = selected ? parseDetail(selected) : null;
|
||||
|
||||
const signalStats = useMemo(() => {
|
||||
const sigs = selectedDetail?.signals ?? [];
|
||||
if (!sigs.length) return null;
|
||||
const buy = sigs.filter(s => s.type === 'BUY' || s.type === 'SHORT_EXIT').length;
|
||||
const sell = sigs.filter(s => s.type === 'SELL' || s.type === 'SHORT_ENTRY' || s.type === 'PARTIAL_SELL').length;
|
||||
return { buy, sell, total: sigs.length };
|
||||
}, [selectedDetail]);
|
||||
|
||||
const leftContent = records.length === 0 ? (
|
||||
<div className="btd-sidebar-empty">
|
||||
<p>백테스팅 이력이 없습니다.</p>
|
||||
<p className="btd-sidebar-hint">차트 화면에서 전략을 선택하고<br />백테스팅을 실행하세요.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="btd-timeline">
|
||||
<span className="btd-timeline-line" />
|
||||
{records.map(r => (
|
||||
<TimelineItem
|
||||
key={r.id}
|
||||
r={r}
|
||||
active={selected?.id === r.id}
|
||||
onClick={() => setSelected(r)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="btd-page">
|
||||
<aside className="btd-sidebar">
|
||||
<div className="btd-sidebar-head">
|
||||
<span className="btd-sidebar-bar" />
|
||||
<h2 className="btd-sidebar-title">백테스팅 실행 이력</h2>
|
||||
<div className="btd-sidebar-actions">
|
||||
<button type="button" className="btd-icon-btn" title="새로고침" onClick={() => void fetchList()}>↻</button>
|
||||
<button type="button" className="btd-icon-btn btd-icon-btn--danger" title="전체 삭제" disabled={records.length === 0} onClick={() => void deleteAll()}>🗑</button>
|
||||
<BuilderPageShell
|
||||
theme={theme}
|
||||
title="백테스팅"
|
||||
subtitle="Backtest History"
|
||||
loading={loading}
|
||||
loadingText="백테스팅 이력 로딩…"
|
||||
leftStorageKey="btd-left-width"
|
||||
footerStorageKey="btd-footer-height"
|
||||
headerActions={(
|
||||
<>
|
||||
<button type="button" className="bps-btn bps-btn--ghost" onClick={() => void fetchList()}>새로고침</button>
|
||||
<button type="button" className="bps-btn bps-btn--danger" disabled={records.length === 0} onClick={() => void deleteAll()}>전체 삭제</button>
|
||||
</>
|
||||
)}
|
||||
leftTitle="실행 이력"
|
||||
left={leftContent}
|
||||
centerHead={selected ? (
|
||||
<span className="bps-center-head-title">{selected.strategyName || '전략 없음'} · {selected.symbol.replace(/^KRW-/, '')}</span>
|
||||
) : undefined}
|
||||
center={selected && selectedDetail ? (
|
||||
<BacktestResultDashboard
|
||||
analysis={selectedDetail.analysis}
|
||||
signals={selectedDetail.signals}
|
||||
strategyName={selected.strategyName}
|
||||
symbol={selected.symbol}
|
||||
timeframe={selected.timeframe}
|
||||
barCount={selected.barCount}
|
||||
createdAt={selected.createdAt}
|
||||
/>
|
||||
) : (
|
||||
<div className="btd-empty">
|
||||
<span>📈</span>
|
||||
<p>좌측 목록에서 백테스팅 결과를 선택하세요.</p>
|
||||
</div>
|
||||
)}
|
||||
rightTitle="결과 요약"
|
||||
right={selected ? (
|
||||
<div className="btd-meta">
|
||||
<div className="bps-card">
|
||||
<div className="bps-card-head">전략</div>
|
||||
<div className="bps-card-body btd-meta-value">{selected.strategyName || '전략 없음'}</div>
|
||||
</div>
|
||||
<div className="bps-card">
|
||||
<div className="bps-card-head">종목 / 타임프레임</div>
|
||||
<div className="bps-card-body btd-meta-value">{selected.symbol.replace(/^KRW-/, '')} · {selected.timeframe}</div>
|
||||
</div>
|
||||
<div className="bps-card">
|
||||
<div className="bps-card-head">실행일 / 봉 수</div>
|
||||
<div className="bps-card-body btd-meta-value">{fmtDate(selected.createdAt)} · {selected.barCount.toLocaleString()}봉</div>
|
||||
</div>
|
||||
<div className="bps-card">
|
||||
<div className="bps-card-head">총 수익률</div>
|
||||
<div className={`bps-card-body btd-meta-value ${(selected.totalReturn ?? 0) >= 0 ? 'up' : 'down'}`}>
|
||||
{pct(selected.totalReturn ?? 0)}
|
||||
</div>
|
||||
</div>
|
||||
{selectedDetail?.analysis && (
|
||||
<div className="bps-card">
|
||||
<div className="bps-card-head">MDD / Sharpe</div>
|
||||
<div className="bps-card-body btd-meta-value">
|
||||
{pct(selectedDetail.analysis.maxDrawdownPct)} · {selectedDetail.analysis.sharpeRatio.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bps-empty">선택된 결과가 없습니다.</div>
|
||||
)}
|
||||
footerLabel={signalStats ? '시그널 요약' : undefined}
|
||||
footer={signalStats ? (
|
||||
<div className="btd-footer-stats">
|
||||
<div className="btd-footer-stat">
|
||||
<span className="btd-footer-stat-label">매수 시그널</span>
|
||||
<span className="btd-footer-stat-value up">{signalStats.buy}</span>
|
||||
</div>
|
||||
<div className="btd-footer-stat">
|
||||
<span className="btd-footer-stat-label">매도 시그널</span>
|
||||
<span className="btd-footer-stat-value down">{signalStats.sell}</span>
|
||||
</div>
|
||||
<div className="btd-footer-stat">
|
||||
<span className="btd-footer-stat-label">총 시그널</span>
|
||||
<span className="btd-footer-stat-value">{signalStats.total}</span>
|
||||
</div>
|
||||
<div className="btd-footer-stat">
|
||||
<span className="btd-footer-stat-label">분석 봉 수</span>
|
||||
<span className="btd-footer-stat-value">{selected?.barCount.toLocaleString() ?? '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="btd-sidebar-empty">로딩 중…</div>
|
||||
) : records.length === 0 ? (
|
||||
<div className="btd-sidebar-empty">
|
||||
<p>백테스팅 이력이 없습니다.</p>
|
||||
<p className="btd-sidebar-hint">차트 화면에서 전략을 선택하고<br />백테스팅을 실행하세요.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="btd-timeline">
|
||||
<span className="btd-timeline-line" />
|
||||
{records.map(r => (
|
||||
<TimelineItem
|
||||
key={r.id}
|
||||
r={r}
|
||||
active={selected?.id === r.id}
|
||||
onClick={() => setSelected(r)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
<main className="btd-main">
|
||||
{selected && selectedDetail ? (
|
||||
<BacktestResultDashboard
|
||||
analysis={selectedDetail.analysis}
|
||||
signals={selectedDetail.signals}
|
||||
strategyName={selected.strategyName}
|
||||
symbol={selected.symbol}
|
||||
timeframe={selected.timeframe}
|
||||
barCount={selected.barCount}
|
||||
createdAt={selected.createdAt}
|
||||
/>
|
||||
) : (
|
||||
<div className="btd-empty">
|
||||
<span>📈</span>
|
||||
<p>좌측 목록에서 백테스팅 결과를 선택하세요.</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
) : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user