import React, { memo, useMemo } from 'react'; import type { EquityPoint, TradeMarker } from '../../utils/backtestEquity'; interface Props { curve: EquityPoint[]; markers: TradeMarker[]; } function fmtDate(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')}`; } function fmtMonth(ts: number): string { const d = new Date(ts * 1000); const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; return `${months[d.getMonth()]} ${d.getFullYear()}`; } interface LineSegment { d: string; up: boolean; } const BacktestAssetChart: React.FC = memo(({ curve, markers }) => { const W = 800; const H = 280; const pad = { t: 22, r: 14, b: 26, l: 48 }; const innerW = W - pad.l - pad.r; const innerH = H - pad.t - pad.b; const chart = useMemo(() => { if (curve.length < 2) { const y = pad.t + innerH * 0.5; return { areaPath: `M${pad.l},${y} L${pad.l + innerW},${y} L${pad.l + innerW},${pad.t + innerH} L${pad.l},${pad.t + innerH} Z`, segments: [] as LineSegment[], yTicks: [0, 1], xLabels: [] as { x: number; label: string }[], markerPts: [] as Array<{ x: number; y: number; m: TradeMarker }>, toY: (_e: number) => y, }; } const times = curve.map(p => p.time); const equities = curve.map(p => p.equity); const tMin = Math.min(...times); const tMax = Math.max(...times); const eMin = Math.min(...equities) * 0.985; const eMax = Math.max(...equities) * 1.015; const tRange = tMax - tMin || 1; const eRange = eMax - eMin || 1; const toX = (t: number) => pad.l + ((t - tMin) / tRange) * innerW; const toY = (e: number) => pad.t + innerH - ((e - eMin) / eRange) * innerH; const linePath = curve.map((p, i) => `${i === 0 ? 'M' : 'L'}${toX(p.time).toFixed(1)},${toY(p.equity).toFixed(1)}`).join(' '); const areaPath = `${linePath} L${toX(curve[curve.length - 1].time).toFixed(1)},${pad.t + innerH} L${toX(curve[0].time).toFixed(1)},${pad.t + innerH} Z`; const segments: LineSegment[] = []; for (let i = 1; i < curve.length; i++) { segments.push({ d: `M${toX(curve[i - 1].time).toFixed(1)},${toY(curve[i - 1].equity).toFixed(1)} L${toX(curve[i].time).toFixed(1)},${toY(curve[i].equity).toFixed(1)}`, up: curve[i].equity >= curve[i - 1].equity, }); } const yTicks = [0, 0.25, 0.5, 0.75, 1].map(r => eMin + eRange * r); const xLabels: { x: number; label: string }[] = []; const step = Math.max(1, Math.floor(curve.length / 5)); for (let i = 0; i < curve.length; i += step) { xLabels.push({ x: toX(curve[i].time), label: i === 0 ? fmtMonth(curve[i].time) : fmtDate(curve[i].time) }); } const last = curve[curve.length - 1]; if (!xLabels.some(l => l.label === fmtDate(last.time))) { xLabels.push({ x: toX(last.time), label: fmtDate(last.time) }); } const markerPts = markers.map(m => ({ x: toX(m.time), y: toY(m.equity), m })); return { areaPath, segments, yTicks, xLabels, markerPts, toY }; }, [curve, markers, innerH, innerW, pad.l, pad.t]); const fmtY = (v: number) => { if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`; if (v >= 10_000) return `${Math.round(v / 10_000)}만`; return Math.round(v).toLocaleString(); }; return (
{[0.25, 0.5, 0.75].map(r => { const y = pad.t + innerH * (1 - r); return ; })} 자본금 {chart.yTicks.map(v => ( {fmtY(v)} ))} {chart.segments.map((seg, i) => ( ))} {chart.markerPts.map(({ x, y, m }, i) => ( {m.type === 'buy' ? ( <> {fmtDate(m.time).slice(5)} ) : ( <> = 0 ? '#22c55e' : '#ef4444'} fontSize="7.5" fontWeight="700"> {m.pnlPct != null ? `${m.pnlPct >= 0 ? '+' : ''}${(m.pnlPct * 100).toFixed(1)}%` : '매도'} )} ))} {chart.xLabels.map((l, i) => ( {l.label} ))}
); }); BacktestAssetChart.displayName = 'BacktestAssetChart'; export default BacktestAssetChart;