import { useState } from 'react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, } from 'recharts'; import useSWR from 'swr'; import { api } from '../lib/api'; import { useLanguage } from '../contexts/LanguageContext'; import { t } from '../i18n/translations'; import { AlertTriangle, BarChart3, DollarSign, Percent, TrendingUp as ArrowUp, TrendingDown as ArrowDown } from 'lucide-react' 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 { language } = useLanguage(); const [displayMode, setDisplayMode] = useState<'dollar' | 'percent'>('dollar'); const { data: history, error } = useSWR( traderId ? `equity-history-${traderId}` : 'equity-history', () => api.getEquityHistory(traderId), { refreshInterval: 30000, // 30秒刷新(历史数据更新频率较低) revalidateOnFocus: false, dedupingInterval: 20000, } ); const { data: account } = useSWR( traderId ? `account-${traderId}` : 'account', () => api.getAccount(traderId), { refreshInterval: 15000, // 15秒刷新(配合后端缓存) revalidateOnFocus: false, dedupingInterval: 10000, } ); if (error) { return (
{t('loadingError', language)}
{error.message}
) } // 过滤掉无效数据:total_equity为0或小于1的数据点(API失败导致) const validHistory = history?.filter(point => point.total_equity > 1) || []; if (!validHistory || validHistory.length === 0) { return (

{t('accountEquityCurve', language)}

{t('noHistoricalData', language)}
{t('dataWillAppear', language)}
) } // 限制显示最近的数据点(性能优化) // 如果数据超过2000个点,只显示最近2000个 const MAX_DISPLAY_POINTS = 2000; const displayHistory = validHistory.length > MAX_DISPLAY_POINTS ? validHistory.slice(-MAX_DISPLAY_POINTS) : validHistory; // 计算初始余额(使用第一个有效数据点,如果无数据则从account获取,最后才用默认值) const initialBalance = validHistory[0]?.total_equity || account?.total_equity || 100; // 默认值改为100,与常见配置一致 // 转换数据格式 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 */}

{t('accountEquityCurve', language)}

{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, }} connectNulls={true} />
{/* Footer Stats */}
{t('initialBalance', language)}
{initialBalance.toFixed(2)} USDT
{t('currentEquity', language)}
{currentValue.raw_equity.toFixed(2)} USDT
{t('historicalCycles', language)}
{validHistory.length} {t('cycles', language)}
{t('displayRange', language)}
{validHistory.length > MAX_DISPLAY_POINTS ? `${t('recent', language)} ${MAX_DISPLAY_POINTS}` : t('allData', language)}
) }