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 = account?.initial_balance || // 从交易员配置读取真实初始余额 (validHistory[0] ? validHistory[0].total_equity - validHistory[0].pnl : undefined) || // 备选:淨值 - 盈亏 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 */}

{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 */}
{/* NOFX Watermark */}
NOFX
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)}
) }