import { useState } from 'react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, } from 'recharts'; import useSWR from 'swr'; import { api } from '../lib/api'; interface EquityPoint { timestamp: string; total_equity: number; pnl: number; pnl_pct: number; cycle_number: number; } interface EquityChartProps { traderId?: string; } export function EquityChart({ traderId }: EquityChartProps) { const [displayMode, setDisplayMode] = useState<'dollar' | 'percent'>('dollar'); const { data: history, error } = useSWR( traderId ? `equity-history-${traderId}` : 'equity-history', () => api.getEquityHistory(traderId), { refreshInterval: 10000, // 每10秒刷新 } ); const { data: account } = useSWR( traderId ? `account-${traderId}` : 'account', () => api.getAccount(traderId), { refreshInterval: 5000, } ); if (error) { return (
⚠️
加载失败
{error.message}
); } if (!history || history.length === 0) { return (

账户净值曲线

📊
暂无历史数据
运行几个周期后将显示收益率曲线
); } // 限制显示最近的数据点(性能优化) // 如果数据超过2000个点,只显示最近2000个 const MAX_DISPLAY_POINTS = 2000; const displayHistory = history.length > MAX_DISPLAY_POINTS ? history.slice(-MAX_DISPLAY_POINTS) : history; // 计算初始余额(使用第一个数据点) const initialBalance = history[0]?.total_equity || 1000; // 转换数据格式 const chartData = displayHistory.map((point) => { const pnl = point.total_equity - initialBalance; const pnlPct = ((pnl / initialBalance) * 100).toFixed(2); return { time: new Date(point.timestamp).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', }), value: displayMode === 'dollar' ? point.total_equity : parseFloat(pnlPct), cycle: point.cycle_number, raw_equity: point.total_equity, raw_pnl: pnl, raw_pnl_pct: parseFloat(pnlPct), }; }); const currentValue = chartData[chartData.length - 1]; const isProfit = currentValue.raw_pnl >= 0; // 计算Y轴范围 const calculateYDomain = () => { if (displayMode === 'percent') { // 百分比模式:找到最大最小值,留20%余量 const values = chartData.map(d => d.value); const minVal = Math.min(...values); const maxVal = Math.max(...values); const range = Math.max(Math.abs(maxVal), Math.abs(minVal)); const padding = Math.max(range * 0.2, 1); // 至少留1%余量 return [Math.floor(minVal - padding), Math.ceil(maxVal + padding)]; } else { // 美元模式:以初始余额为基准,上下留10%余量 const values = chartData.map(d => d.value); const minVal = Math.min(...values, initialBalance); const maxVal = Math.max(...values, initialBalance); const range = maxVal - minVal; const padding = Math.max(range * 0.15, initialBalance * 0.01); // 至少留1%余量 return [ Math.floor(minVal - padding), Math.ceil(maxVal + padding) ]; } }; // 自定义Tooltip - Binance Style const CustomTooltip = ({ active, payload }: any) => { if (active && payload && payload.length) { const data = payload[0].payload; return (
Cycle #{data.cycle}
{data.raw_equity.toFixed(2)} USDT
= 0 ? '#0ECB81' : '#F6465D' }} > {data.raw_pnl >= 0 ? '+' : ''} {data.raw_pnl.toFixed(2)} USDT ({data.raw_pnl_pct >= 0 ? '+' : ''} {data.raw_pnl_pct}%)
); } return null; }; return (
{/* Header */}

账户净值曲线

{account?.total_equity.toFixed(2) || '0.00'} USDT
{isProfit ? '▲' : '▼'} {isProfit ? '+' : ''} {currentValue.raw_pnl_pct}% ({isProfit ? '+' : ''}{currentValue.raw_pnl.toFixed(2)} USDT)
{/* Display Mode Toggle */}
{/* Chart */}
displayMode === 'dollar' ? `$${value.toFixed(0)}` : `${value}%` } /> } /> 50 ? false : { fill: '#F0B90B', r: 3 }} activeDot={{ r: 6, fill: '#FCD535', stroke: '#F0B90B', strokeWidth: 2 }} />
{/* Footer Stats */}
初始余额
{initialBalance.toFixed(2)} USDT
当前净值
{currentValue.raw_equity.toFixed(2)} USDT
历史周期
{history.length} 个
显示范围
{history.length > MAX_DISPLAY_POINTS ? `最近 ${MAX_DISPLAY_POINTS}` : '全部数据' }
); }