import React, { useState, useEffect } from 'react' import { useSelector } from 'react-redux' import { api } from '../services/api' import { selectAccountId } from '../store/appSlice' import './TradeList.css' const TradeList = () => { const accountId = useSelector(selectAccountId) // 从 Redux 获取当前账号ID const [trades, setTrades] = useState([]) const [stats, setStats] = useState(null) const [loading, setLoading] = useState(true) // 筛选状态 const [period, setPeriod] = useState('today') // '1d', '7d', '30d', 'today', 'week', 'month', null const [startDate, setStartDate] = useState('') const [endDate, setEndDate] = useState('') const [symbol, setSymbol] = useState('') const [status, setStatus] = useState('') const [useCustomDate, setUseCustomDate] = useState(false) const [tradeType, setTradeType] = useState('') const [exitReason, setExitReason] = useState('') useEffect(() => { loadData() }, [accountId]) // 当 accountId 变化时重新加载 const loadData = async () => { setLoading(true) try { const params = { limit: 100 } // 如果使用快速时间段筛选 if (!useCustomDate && period) { params.period = period } else if (useCustomDate) { // 使用自定义日期 if (startDate) params.start_date = startDate if (endDate) params.end_date = endDate } if (symbol) params.symbol = symbol if (status) params.status = status if (tradeType) params.trade_type = tradeType if (exitReason) params.exit_reason = exitReason const [tradesData, statsData] = await Promise.all([ api.getTrades(params), api.getTradeStats(params) ]) setTrades(tradesData.trades || []) setStats(statsData) } catch (error) { console.error('Failed to load trades:', error) } finally { setLoading(false) } } const handlePeriodChange = (newPeriod) => { setPeriod(newPeriod) setUseCustomDate(false) setStartDate('') setEndDate('') } const handleCustomDateToggle = () => { setUseCustomDate(!useCustomDate) if (!useCustomDate) { setPeriod(null) } } const handleReset = () => { setPeriod(null) setStartDate('') setEndDate('') setSymbol('') setStatus('') setUseCustomDate(false) } // 导出当前订单数据(含入场/离场原因、入场思路等完整字段,便于后续分析) const handleExport = () => { if (trades.length === 0) { alert('暂无数据可导出') return } const exportData = trades.map(trade => { const notional = trade.notional_usdt !== undefined && trade.notional_usdt !== null ? parseFloat(trade.notional_usdt) : (trade.entry_value_usdt !== undefined && trade.entry_value_usdt !== null ? parseFloat(trade.entry_value_usdt) : (parseFloat(trade.quantity || 0) * parseFloat(trade.entry_price || 0))) const leverage = parseFloat(trade.leverage || 10) const margin = trade.margin_usdt !== undefined && trade.margin_usdt !== null ? parseFloat(trade.margin_usdt) : (leverage > 0 ? notional / leverage : 0) const pnl = parseFloat(trade.pnl || 0) const pnlPercent = margin > 0 ? (pnl / margin) * 100 : 0 const row = { 交易ID: trade.id, 交易对: trade.symbol, 方向: trade.side, 数量: parseFloat(trade.quantity || 0), 名义价值: notional, 保证金: margin, 杠杆: leverage, 入场价: parseFloat(trade.entry_price || 0), 出场价: trade.exit_price ? parseFloat(trade.exit_price) : null, 盈亏: pnl, 盈亏比例: pnlPercent, 状态: trade.status === 'open' ? '持仓中' : trade.status === 'closed' ? '已平仓' : '已取消', 平仓类型: trade.exit_reason_display || '-', 开仓订单号: trade.entry_order_id || '-', 平仓订单号: trade.exit_order_id || '-', 入场时间: trade.entry_time, 平仓时间: trade.exit_time || null, // 以下为分析用完整字段 入场原因: trade.entry_reason ?? null, 离场原因: trade.exit_reason ?? null, 持仓时长分钟: trade.duration_minutes ?? null, 止损价: trade.stop_loss_price != null ? parseFloat(trade.stop_loss_price) : null, 止盈价: trade.take_profit_price != null ? parseFloat(trade.take_profit_price) : null, 第一目标止盈价: trade.take_profit_1 != null ? parseFloat(trade.take_profit_1) : null, 第二目标止盈价: trade.take_profit_2 != null ? parseFloat(trade.take_profit_2) : null, ATR: trade.atr != null ? parseFloat(trade.atr) : null, 策略类型: trade.strategy_type ?? null, } if (trade.entry_context != null) { row.入场思路 = trade.entry_context } return row }) // 生成文件名 const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-') const filename = `交易记录_${timestamp}.json` // 创建并下载文件 const dataStr = JSON.stringify(exportData, null, 2) const dataBlob = new Blob([dataStr], { type: 'application/json' }) const url = URL.createObjectURL(dataBlob) const link = document.createElement('a') link.href = url link.download = filename document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(url) } if (loading) return
加载中...
return (

交易记录

说明:每条记录代表一笔完整的交易(开仓+平仓),统计总盈亏时每条记录只计算一次

{/* 筛选面板 */}
{useCustomDate && (
setStartDate(e.target.value)} placeholder="开始日期" /> setEndDate(e.target.value)} placeholder="结束日期" min={startDate} />
)}
setSymbol(e.target.value)} placeholder="如: BTCUSDT" style={{ width: '150px' }} />
{trades.length > 0 && ( )}
{ stats && (
整体统计
总交易数:{stats.total_trades}
胜率:{stats.win_rate.toFixed(2)}%
总盈亏:{stats.total_pnl.toFixed(2)} USDT
平均盈亏:{stats.avg_pnl.toFixed(2)} USDT
平均持仓时长(分钟):{stats.avg_duration_minutes ? Number(stats.avg_duration_minutes).toFixed(0) : 0}
平仓原因(有意义交易):
{(() => { const m = stats.exit_reason_counts || {} const stopLoss = Number(m.stop_loss || 0) const takeProfit = Number(m.take_profit || 0) const trailing = Number(m.trailing_stop || 0) const manual = Number(m.manual || 0) const sync = Number(m.sync || 0) const other = Number(m.unknown || 0) const parts = [] if (stopLoss) parts.push(`止损 ${stopLoss}`) if (takeProfit) parts.push(`止盈 ${takeProfit}`) if (trailing) parts.push(`移动止损 ${trailing}`) if (manual) parts.push(`手动 ${manual}`) if (sync) parts.push(`同步 ${sync}`) if (other) parts.push(`其他 ${other}`) return parts.length ? parts.join(' / ') : '—' })()}
平均盈利 / 平均亏损(期望 3:1):{Number(stats.avg_win_loss_ratio || 0).toFixed(2)} : 1
总交易量(名义):{Number(stats.total_notional_usdt || 0).toFixed(2)} USDT
) } { stats && (
总交易数
{stats.total_trades}
{stats.meaningful_trades !== undefined && ( <>(有意义: {stats.meaningful_trades},0盈亏: {stats.zero_pnl_trades || 0}) )} {stats.meaningful_trades === undefined && <>(已平仓的完整交易)}
胜率
{stats.win_rate.toFixed(2)}%
{stats.meaningful_trades !== undefined && <>(已排除0盈亏订单)}
总盈亏
= 0 ? 'positive' : 'negative'}`}> {stats.total_pnl.toFixed(2)} USDT
平均盈亏
= 0 ? 'positive' : 'negative'}`}> {stats.avg_pnl.toFixed(2)} USDT
{"avg_duration_minutes" in stats && stats.avg_duration_minutes !== null && stats.avg_duration_minutes !== undefined && (
平均持仓时长(分钟)
{Number(stats.avg_duration_minutes || 0).toFixed(0)}
(仅统计“有意义交易”;优先使用 duration_minutes,缺失时用 exit_time-entry_time 计算)
)} {"exit_reason_counts" in stats && stats.exit_reason_counts && (
平仓原因(有意义交易)
{(() => { const m = stats.exit_reason_counts || {} const stopLoss = Number(m.stop_loss || 0) const takeProfit = Number(m.take_profit || 0) const trailing = Number(m.trailing_stop || 0) const manual = Number(m.manual || 0) const sync = Number(m.sync || 0) const other = Number(m.unknown || 0) const parts = [] if (stopLoss) parts.push(`止损 ${stopLoss}`) if (takeProfit) parts.push(`止盈 ${takeProfit}`) if (trailing) parts.push(`移动止损 ${trailing}`) if (manual) parts.push(`手动 ${manual}`) if (sync) parts.push(`同步 ${sync}`) if (other) parts.push(`其他 ${other}`) return parts.length ? parts.join(' / ') : '—' })()}
)} {"avg_win_pnl" in stats && "avg_loss_pnl_abs" in stats && Number(stats.total_pnl || 0) > 0 && (
平均盈利 / 平均亏损(期望 3:1)
= 3 ? 'positive' : '' }`} > {typeof stats.avg_win_loss_ratio === 'number' ? `${stats.avg_win_loss_ratio.toFixed(2)} : 1` : '—'}
+{Number(stats.avg_win_pnl || 0).toFixed(2)} / -{Number(stats.avg_loss_pnl_abs || 0).toFixed(2)} USDT
)} {"total_notional_usdt" in stats && (
总交易量(名义)
{Number(stats.total_notional_usdt || 0).toFixed(2)} USDT
(口径:入场价×数量)
)}
) } { trades.length === 0 ? (
暂无交易记录
) : ( <> {/* 桌面端表格:用横向滚动包裹,避免整页过宽 */}
{trades.map(trade => { // 名义/保证金:优先使用后端返回字段(notional_usdt / margin_usdt),否则回退计算 const notional = trade.notional_usdt !== undefined && trade.notional_usdt !== null ? parseFloat(trade.notional_usdt) : ( trade.entry_value_usdt !== undefined && trade.entry_value_usdt !== null ? parseFloat(trade.entry_value_usdt) : (parseFloat(trade.quantity || 0) * parseFloat(trade.entry_price || 0)) ) const leverage = parseFloat(trade.leverage || 10) const margin = trade.margin_usdt !== undefined && trade.margin_usdt !== null ? parseFloat(trade.margin_usdt) : (leverage > 0 ? notional / leverage : 0) // 计算盈亏比例(盈亏/保证金) const pnl = parseFloat(trade.pnl || 0) const pnlPercent = margin > 0 ? (pnl / margin) * 100 : 0 // 格式化时间为北京时间 // 支持Unix时间戳(秒数)或日期字符串 const formatTime = (timeValue) => { if (!timeValue) return '-' try { let date // 如果是数字(Unix时间戳),转换为毫秒 if (typeof timeValue === 'number') { date = new Date(timeValue * 1000) } else { date = new Date(timeValue) } if (isNaN(date.getTime())) return String(timeValue) return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', timeZone: 'Asia/Shanghai' }) } catch (e) { return String(timeValue) } } // 格式化订单号显示 const formatOrderIds = () => { const entry = trade.entry_order_id || '-' const exit = trade.exit_order_id || '-' if (entry === '-' && exit === '-') return '-' if (entry !== '-' && exit !== '-') { return `开仓: ${entry} / 平仓: ${exit}` } return entry !== '-' ? `开仓: ${entry}` : `平仓: ${exit}` } return ( ) })}
交易ID 交易对 方向 数量 名义 保证金 入场价 出场价 盈亏 盈亏比例 状态 平仓类型 币安订单号 入场时间 平仓时间
#{trade.id} {trade.symbol} {trade.side} {parseFloat(trade.quantity).toFixed(4)} {notional >= 0.01 ? notional.toFixed(2) : notional.toFixed(4)} USDT {margin >= 0.01 ? margin.toFixed(2) : margin.toFixed(4)} USDT {parseFloat(trade.entry_price).toFixed(4)} {trade.exit_price ? parseFloat(trade.exit_price).toFixed(4) : '-'} = 0 ? 'positive' : 'negative'}> {pnl.toFixed(2)} USDT = 0 ? 'positive' : 'negative'}> {pnlPercent >= 0 ? '+' : ''}{pnlPercent.toFixed(2)}% {trade.status === 'open' ? '持仓中' : trade.status === 'closed' ? '已平仓' : '已取消'} {trade.exit_reason_display || '-'} {formatOrderIds()} {formatTime(trade.entry_time)} {trade.exit_time ? formatTime(trade.exit_time) : '-'}
{/* 移动端卡片 */}
{trades.map(trade => { // 名义/保证金:优先后端字段 const notional = trade.notional_usdt !== undefined && trade.notional_usdt !== null ? parseFloat(trade.notional_usdt) : ( trade.entry_value_usdt !== undefined && trade.entry_value_usdt !== null ? parseFloat(trade.entry_value_usdt) : (parseFloat(trade.quantity || 0) * parseFloat(trade.entry_price || 0)) ) const leverage = parseFloat(trade.leverage || 10) const margin = trade.margin_usdt !== undefined && trade.margin_usdt !== null ? parseFloat(trade.margin_usdt) : (leverage > 0 ? notional / leverage : 0) const pnl = parseFloat(trade.pnl || 0) const pnlPercent = margin > 0 ? (pnl / margin) * 100 : 0 // 格式化时间为北京时间 // 支持Unix时间戳(秒数)或日期字符串 const formatTime = (timeValue) => { if (!timeValue) return '-' try { let date // 如果是数字(Unix时间戳),转换为毫秒 if (typeof timeValue === 'number') { date = new Date(timeValue * 1000) } else { date = new Date(timeValue) } if (isNaN(date.getTime())) return String(timeValue) return date.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', timeZone: 'Asia/Shanghai' }) } catch (e) { return String(timeValue) } } return (
{trade.symbol} 交易ID: #{trade.id} {trade.side === 'BUY' ? '买入' : '卖出'} · {trade.status === 'open' ? '持仓中' : trade.status === 'closed' ? '已平仓' : '已取消'}
数量 {parseFloat(trade.quantity).toFixed(4)}
名义 {notional >= 0.01 ? notional.toFixed(2) : notional.toFixed(4)} USDT
保证金 {margin >= 0.01 ? margin.toFixed(2) : margin.toFixed(4)} USDT
入场价 {parseFloat(trade.entry_price).toFixed(4)}
出场价 {trade.exit_price ? parseFloat(trade.exit_price).toFixed(4) : '-'}
盈亏 = 0 ? 'positive' : 'negative'}`}> {pnl.toFixed(2)} USDT
盈亏比例 = 0 ? 'positive' : 'negative'}`}> {pnlPercent >= 0 ? '+' : ''}{pnlPercent.toFixed(2)}%
{trade.exit_reason_display && (
平仓类型 {trade.exit_reason_display}
)} {(trade.entry_order_id || trade.exit_order_id) && (
币安订单号 {trade.entry_order_id ? `开仓: ${trade.entry_order_id}` : ''} {trade.entry_order_id && trade.exit_order_id ? ' / ' : ''} {trade.exit_order_id ? `平仓: ${trade.exit_order_id}` : ''}
)}
入场: {formatTime(trade.entry_time)}
{trade.exit_time && (
平仓: {formatTime(trade.exit_time)}
)}
) })}
) }
) } export default TradeList