Files
goldenChart/frontend/src/components/backtest/BacktestResultDashboard.tsx
T
2026-05-23 21:08:46 +09:00

166 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* BacktestResultDashboard — 첨부 이미지 스타일 백테스팅 결과 대시보드
*/
import React, { useMemo } from 'react';
import type { BacktestAnalysis, BacktestSignal } from '../../utils/backendApi';
import { buildEquityFromSignals, type TradeHistoryRow } from '../../utils/backtestEquity';
import BacktestAssetChart from './BacktestAssetChart';
export interface BacktestResultDashboardProps {
analysis: BacktestAnalysis | null;
signals: BacktestSignal[];
strategyName: string;
symbol: string;
timeframe: string;
barCount: number;
createdAt?: string;
}
const pct = (v: number, dec = 1) =>
isFinite(v) && v !== 0 ? `${v >= 0 ? '+' : ''}${(v * 100).toFixed(dec)}%` : '0.0%';
const pctAbs = (v: number, dec = 0) =>
isFinite(v) ? `${(v * 100).toFixed(dec)}%` : '';
const num = (v: number, dec = 2) =>
isFinite(v) ? v.toFixed(dec) : '';
function fmtDateTime(ts: number): string {
const d = new Date(ts * 1000);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}
function SectionTitle({ title, sub }: { title: string; sub?: string }) {
return (
<div className="btd-section-head">
<span className="btd-section-bar" />
<div>
<h2 className="btd-section-title">{title}</h2>
{sub && <p className="btd-section-sub">{sub}</p>}
</div>
</div>
);
}
export function BacktestResultDashboard({
analysis,
signals,
strategyName: _strategyName,
symbol,
timeframe: _timeframe,
barCount: _barCount,
createdAt: _createdAt,
}: BacktestResultDashboardProps) {
const a = analysis;
const { curve, markers, trades } = useMemo(() => {
if (!a) return { curve: [], markers: [], trades: [] };
return buildEquityFromSignals(signals, a.initialCapital, symbol.startsWith('KRW-') ? symbol : `KRW-${symbol.replace(/^KRW-/, '')}`);
}, [a, signals, symbol]);
if (!a) {
return (
<div className="btd-empty">
<span>📊</span>
<p>결과 데이터가 없습니다.</p>
</div>
);
}
const profitFactor = a.profitLossRatio > 0 ? a.profitLossRatio : (
a.grossLoss !== 0 ? Math.abs(a.grossProfit / a.grossLoss) : 0
);
return (
<div className="btd-dashboard">
<div className="btd-section btd-section--kpi">
<SectionTitle title="핵심 성과 지표" sub="Core KPIs" />
<div className="btd-kpi-grid">
<div className="btd-kpi-card">
<div className="btd-kpi-label"> 수익률 (ROI)</div>
<div className={`btd-kpi-value ${a.totalReturnPct >= 0 ? 'up' : 'down'}`}>
{pct(a.totalReturnPct)}
</div>
<p className="btd-kpi-desc">초기 자본 대비 최종 수익</p>
</div>
<div className="btd-kpi-card">
<div className="btd-kpi-label">최대 낙폭 (MDD)</div>
<div className={`btd-kpi-value ${a.maxDrawdownPct <= 0 ? 'down' : ''}`}>
{pct(a.maxDrawdownPct)}
</div>
<p className="btd-kpi-desc">기간 최대 손실폭</p>
</div>
<div className="btd-kpi-card btd-kpi-card--sharpe">
<div className="btd-kpi-label">샤프 비율 (Sharpe Ratio)</div>
<div className="btd-kpi-row">
<span className={`btd-kpi-value ${a.sharpeRatio >= 1 ? 'up' : a.sharpeRatio >= 0 ? 'neutral' : 'down'}`}>{num(a.sharpeRatio)}</span>
<span className="btd-kpi-icon" aria-hidden></span>
</div>
<p className="btd-kpi-desc">위험 대비 초과 수익</p>
</div>
<div className="btd-kpi-card btd-kpi-card--dual">
<div className="btd-kpi-label">승률 Profit Factor</div>
<div className="btd-kpi-dual">
<div className="btd-kpi-value up">{pctAbs(a.winRate)}</div>
<div className="btd-kpi-divider" />
<div className="btd-kpi-value gold">{num(profitFactor)}</div>
</div>
</div>
</div>
</div>
<div className="btd-section btd-section--chart">
<SectionTitle title="자산 곡선 및 차트 시각화" sub="Asset Curve" />
<div className="btd-chart-card">
<BacktestAssetChart curve={curve} markers={markers} />
</div>
</div>
<div className="btd-section btd-section--table">
<SectionTitle title="상세 거래 내역" sub="Trade History Table" />
<div className="btd-table-card">
<div className="btd-table-scroll">
<table className="btd-table">
<thead>
<tr>
<th>거래 일시</th>
<th>종목</th>
<th>유형</th>
<th>체결가</th>
<th>수량</th>
<th>손익 (%)</th>
</tr>
</thead>
<tbody>
{trades.length === 0 ? (
<tr>
<td colSpan={6} className="btd-table-empty">거래 내역이 없습니다.</td>
</tr>
) : trades.map((t: TradeHistoryRow) => (
<tr key={t.id}>
<td className="btd-td-time">{fmtDateTime(t.time)}</td>
<td>{t.symbol.startsWith('KRW-') ? t.symbol : `KRW-${t.symbol}`}</td>
<td className={t.side === 'buy' ? 'buy' : 'sell'}>
{t.side === 'buy' ? '매수' : '매도'}
</td>
<td>{Math.round(t.price).toLocaleString()}</td>
<td>{t.quantity.toFixed(4)}</td>
<td className={t.pnlPct != null ? (t.pnlPct >= 0 ? 'up' : 'down') : ''}>
{t.pnlPct != null ? pct(t.pnlPct) : ''}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
}
export default BacktestResultDashboard;