a
This commit is contained in:
parent
078576ddf8
commit
d914b64294
|
|
@ -240,366 +240,374 @@ const TradeList = () => {
|
||||||
|
|
||||||
{
|
{
|
||||||
stats && (
|
stats && (
|
||||||
<div>
|
<div >
|
||||||
<div>整体统计</div>
|
<div style={{ fontSize: '1.1rem' }}>整体统计</div>
|
||||||
<div>总交易数:{stats.total_trades}</div>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||||
<div>胜率:{stats.win_rate.toFixed(2)}%</div>
|
<div style={{ fontSize: '1.1rem' }}>总交易数:{stats.total_trades} </div>
|
||||||
<div>总盈亏:{stats.total_pnl.toFixed(2)} USDT</div>
|
<div style={{ fontSize: '1.1rem' }}>胜率:{stats.win_rate.toFixed(2)}%</div>
|
||||||
<div>平均盈亏:{stats.avg_pnl.toFixed(2)} USDT</div>
|
</div>
|
||||||
<div>平均持仓时长(分钟):{stats.avg_duration_minutes ? Number(stats.avg_duration_minutes).toFixed(0) : 0}</div>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||||
<div>平仓原因(有意义交易):
|
<div style={{ fontSize: '1.1rem' }}>总盈亏:{stats.total_pnl.toFixed(2)} USDT</div>
|
||||||
<div style={{ fontSize: '1.1rem' }}>
|
<div style={{ fontSize: '1.1rem' }}>平均盈亏:{stats.avg_pnl.toFixed(2)} USDT</div>
|
||||||
{(() => {
|
</div>
|
||||||
const m = stats.exit_reason_counts || {}
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||||
const stopLoss = Number(m.stop_loss || 0)
|
<div style={{ fontSize: '1.1rem' }}>平均持仓时长(分钟):{stats.avg_duration_minutes ? Number(stats.avg_duration_minutes).toFixed(0) : 0}</div>
|
||||||
const takeProfit = Number(m.take_profit || 0)
|
<div style={{ fontSize: '1.1rem' }}>平仓原因(有意义交易):
|
||||||
const trailing = Number(m.trailing_stop || 0)
|
<div style={{ fontSize: '1.1rem' }}>
|
||||||
const manual = Number(m.manual || 0)
|
{(() => {
|
||||||
const sync = Number(m.sync || 0)
|
const m = stats.exit_reason_counts || {}
|
||||||
const other = Number(m.unknown || 0)
|
const stopLoss = Number(m.stop_loss || 0)
|
||||||
const parts = []
|
const takeProfit = Number(m.take_profit || 0)
|
||||||
if (stopLoss) parts.push(`止损 ${stopLoss}`)
|
const trailing = Number(m.trailing_stop || 0)
|
||||||
if (takeProfit) parts.push(`止盈 ${takeProfit}`)
|
const manual = Number(m.manual || 0)
|
||||||
if (trailing) parts.push(`移动止损 ${trailing}`)
|
const sync = Number(m.sync || 0)
|
||||||
if (manual) parts.push(`手动 ${manual}`)
|
const other = Number(m.unknown || 0)
|
||||||
if (sync) parts.push(`同步 ${sync}`)
|
const parts = []
|
||||||
if (other) parts.push(`其他 ${other}`)
|
if (stopLoss) parts.push(`止损 ${stopLoss}`)
|
||||||
return parts.length ? parts.join(' / ') : '—'
|
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(' / ') : '—'
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-value">平均盈利 / 平均亏损(期望 3:1):{Number(stats.avg_win_loss_ratio || 0).toFixed(2)} : 1</div>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||||
<div className="stat-value">总交易量(名义):{Number(stats.total_notional_usdt || 0).toFixed(2)} USDT</div>
|
<div style={{ fontSize: '1.1rem' }}>平均盈利 / 平均亏损(期望 3:1):{Number(stats.avg_win_loss_ratio || 0).toFixed(2)} : 1</div>
|
||||||
</div>
|
<div style={{ fontSize: '1.1rem' }}>总交易量(名义):{Number(stats.total_notional_usdt || 0).toFixed(2)} USDT</div>
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
stats && (
|
|
||||||
<div className="stats-summary">
|
|
||||||
<div className="stat-card">
|
|
||||||
<div className="stat-label">总交易数</div>
|
|
||||||
<div className="stat-value">{stats.total_trades}</div>
|
|
||||||
<div style={{ fontSize: '12px', color: '#999', marginTop: '4px' }}>
|
|
||||||
{stats.meaningful_trades !== undefined && (
|
|
||||||
<>(有意义: {stats.meaningful_trades},0盈亏: {stats.zero_pnl_trades || 0})</>
|
|
||||||
)}
|
|
||||||
{stats.meaningful_trades === undefined && <>(已平仓的完整交易)</>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="stat-card">
|
|
||||||
<div className="stat-label">胜率</div>
|
|
||||||
<div className="stat-value">{stats.win_rate.toFixed(2)}%</div>
|
|
||||||
<div style={{ fontSize: '12px', color: '#999', marginTop: '4px' }}>
|
|
||||||
{stats.meaningful_trades !== undefined && <>(已排除0盈亏订单)</>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="stat-card">
|
|
||||||
<div className="stat-label">总盈亏</div>
|
|
||||||
<div className={`stat-value ${stats.total_pnl >= 0 ? 'positive' : 'negative'}`}>
|
|
||||||
{stats.total_pnl.toFixed(2)} USDT
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="stat-card">
|
|
||||||
<div className="stat-label">平均盈亏</div>
|
|
||||||
<div className={`stat-value ${stats.avg_pnl >= 0 ? 'positive' : 'negative'}`}>
|
|
||||||
{stats.avg_pnl.toFixed(2)} USDT
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{"avg_duration_minutes" in stats && stats.avg_duration_minutes !== null && stats.avg_duration_minutes !== undefined && (
|
|
||||||
<div className="stat-card">
|
|
||||||
<div className="stat-label">平均持仓时长(分钟)</div>
|
|
||||||
<div className="stat-value">{Number(stats.avg_duration_minutes || 0).toFixed(0)}</div>
|
|
||||||
<div style={{ fontSize: '12px', color: '#999', marginTop: '4px' }}>
|
|
||||||
(仅统计“有意义交易”;优先使用 duration_minutes,缺失时用 exit_time-entry_time 计算)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{"exit_reason_counts" in stats && stats.exit_reason_counts && (
|
|
||||||
<div className="stat-card">
|
|
||||||
<div className="stat-label">平仓原因(有意义交易)</div>
|
|
||||||
<div className="stat-value" style={{ fontSize: '1.1rem' }}>
|
|
||||||
{(() => {
|
|
||||||
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(' / ') : '—'
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{"avg_win_pnl" in stats && "avg_loss_pnl_abs" in stats && Number(stats.total_pnl || 0) > 0 && (
|
|
||||||
<div className="stat-card">
|
|
||||||
<div className="stat-label">平均盈利 / 平均亏损(期望 3:1)</div>
|
|
||||||
<div
|
|
||||||
className={`stat-value ${typeof stats.avg_win_loss_ratio === 'number' && stats.avg_win_loss_ratio >= 3 ? 'positive' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{typeof stats.avg_win_loss_ratio === 'number'
|
|
||||||
? `${stats.avg_win_loss_ratio.toFixed(2)} : 1`
|
|
||||||
: '—'}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '12px', color: '#999', marginTop: '4px' }}>
|
|
||||||
+{Number(stats.avg_win_pnl || 0).toFixed(2)} / -{Number(stats.avg_loss_pnl_abs || 0).toFixed(2)} USDT
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{"total_notional_usdt" in stats && (
|
|
||||||
<div className="stat-card">
|
|
||||||
<div className="stat-label">总交易量(名义)</div>
|
|
||||||
<div className="stat-value">{Number(stats.total_notional_usdt || 0).toFixed(2)} USDT</div>
|
|
||||||
<div style={{ fontSize: '12px', color: '#999', marginTop: '4px' }}>
|
|
||||||
(口径:入场价×数量)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
trades.length === 0 ? (
|
|
||||||
<div className="no-data">暂无交易记录</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{/* 桌面端表格 */}
|
|
||||||
<table className="trades-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>交易ID</th>
|
|
||||||
<th>交易对</th>
|
|
||||||
<th>方向</th>
|
|
||||||
<th>数量</th>
|
|
||||||
<th>名义</th>
|
|
||||||
<th>保证金</th>
|
|
||||||
<th>入场价</th>
|
|
||||||
<th>出场价</th>
|
|
||||||
<th>盈亏</th>
|
|
||||||
<th>盈亏比例</th>
|
|
||||||
<th>状态</th>
|
|
||||||
<th>平仓类型</th>
|
|
||||||
<th>币安订单号</th>
|
|
||||||
<th>入场时间</th>
|
|
||||||
<th>平仓时间</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{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 (
|
|
||||||
<tr key={trade.id}>
|
|
||||||
<td style={{ fontSize: '12px', color: '#999' }}>#{trade.id}</td>
|
|
||||||
<td>{trade.symbol}</td>
|
|
||||||
<td className={trade.side === 'BUY' ? 'buy' : 'sell'}>{trade.side}</td>
|
|
||||||
<td>{parseFloat(trade.quantity).toFixed(4)}</td>
|
|
||||||
<td>{notional >= 0.01 ? notional.toFixed(2) : notional.toFixed(4)} USDT</td>
|
|
||||||
<td>{margin >= 0.01 ? margin.toFixed(2) : margin.toFixed(4)} USDT</td>
|
|
||||||
<td>{parseFloat(trade.entry_price).toFixed(4)}</td>
|
|
||||||
<td>{trade.exit_price ? parseFloat(trade.exit_price).toFixed(4) : '-'}</td>
|
|
||||||
<td className={pnl >= 0 ? 'positive' : 'negative'}>
|
|
||||||
{pnl.toFixed(2)} USDT
|
|
||||||
</td>
|
|
||||||
<td className={pnlPercent >= 0 ? 'positive' : 'negative'}>
|
|
||||||
{pnlPercent >= 0 ? '+' : ''}{pnlPercent.toFixed(2)}%
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span className={`status ${trade.status}`}>{trade.status === 'open' ? '持仓中' : trade.status === 'closed' ? '已平仓' : '已取消'}</span>
|
|
||||||
</td>
|
|
||||||
<td>{trade.exit_reason_display || '-'}</td>
|
|
||||||
<td className="order-id" style={{ fontSize: '12px' }}>{formatOrderIds()}</td>
|
|
||||||
<td>{formatTime(trade.entry_time)}</td>
|
|
||||||
<td>{trade.exit_time ? formatTime(trade.exit_time) : '-'}</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{/* 移动端卡片 */}
|
|
||||||
<div className="trades-cards">
|
|
||||||
{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 (
|
|
||||||
<div key={trade.id} className="trade-card">
|
|
||||||
<div className="trade-card-header">
|
|
||||||
<span className="trade-card-symbol">{trade.symbol}</span>
|
|
||||||
<span style={{ fontSize: '11px', color: '#999', marginLeft: '8px' }}>交易ID: #{trade.id}</span>
|
|
||||||
<span className={`trade-card-side ${trade.side === 'BUY' ? 'buy' : 'sell'} status ${trade.status}`}>
|
|
||||||
{trade.side === 'BUY' ? '买入' : '卖出'} · {trade.status === 'open' ? '持仓中' : trade.status === 'closed' ? '已平仓' : '已取消'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="trade-card-body">
|
|
||||||
<div className="trade-card-field">
|
|
||||||
<span className="trade-card-label">数量</span>
|
|
||||||
<span className="trade-card-value">{parseFloat(trade.quantity).toFixed(4)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="trade-card-field">
|
|
||||||
<span className="trade-card-label">名义</span>
|
|
||||||
<span className="trade-card-value">{notional >= 0.01 ? notional.toFixed(2) : notional.toFixed(4)} USDT</span>
|
|
||||||
</div>
|
|
||||||
<div className="trade-card-field">
|
|
||||||
<span className="trade-card-label">保证金</span>
|
|
||||||
<span className="trade-card-value">{margin >= 0.01 ? margin.toFixed(2) : margin.toFixed(4)} USDT</span>
|
|
||||||
</div>
|
|
||||||
<div className="trade-card-field">
|
|
||||||
<span className="trade-card-label">入场价</span>
|
|
||||||
<span className="trade-card-value">{parseFloat(trade.entry_price).toFixed(4)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="trade-card-field">
|
|
||||||
<span className="trade-card-label">出场价</span>
|
|
||||||
<span className="trade-card-value">{trade.exit_price ? parseFloat(trade.exit_price).toFixed(4) : '-'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="trade-card-field">
|
|
||||||
<span className="trade-card-label">盈亏</span>
|
|
||||||
<span className={`trade-card-value ${pnl >= 0 ? 'positive' : 'negative'}`}>
|
|
||||||
{pnl.toFixed(2)} USDT
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="trade-card-field">
|
|
||||||
<span className="trade-card-label">盈亏比例</span>
|
|
||||||
<span className={`trade-card-value ${pnlPercent >= 0 ? 'positive' : 'negative'}`}>
|
|
||||||
{pnlPercent >= 0 ? '+' : ''}{pnlPercent.toFixed(2)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{trade.exit_reason_display && (
|
|
||||||
<div className="trade-card-field">
|
|
||||||
<span className="trade-card-label">平仓类型</span>
|
|
||||||
<span className="trade-card-value">{trade.exit_reason_display}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{(trade.entry_order_id || trade.exit_order_id) && (
|
|
||||||
<div className="trade-card-field">
|
|
||||||
<span className="trade-card-label">币安订单号</span>
|
|
||||||
<span className="trade-card-value order-id" style={{ fontSize: '12px' }}>
|
|
||||||
{trade.entry_order_id ? `开仓: ${trade.entry_order_id}` : ''}
|
|
||||||
{trade.entry_order_id && trade.exit_order_id ? ' / ' : ''}
|
|
||||||
{trade.exit_order_id ? `平仓: ${trade.exit_order_id}` : ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="trade-card-footer">
|
|
||||||
<div className="trade-time-item">
|
|
||||||
<span className="time-label">入场:</span>
|
|
||||||
<span>{formatTime(trade.entry_time)}</span>
|
|
||||||
</div>
|
|
||||||
{trade.exit_time && (
|
|
||||||
<div className="trade-time-item">
|
|
||||||
<span className="time-label">平仓:</span>
|
|
||||||
<span>{formatTime(trade.exit_time)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
}
|
||||||
</div>
|
|
||||||
</>
|
{
|
||||||
)
|
stats && (
|
||||||
}
|
<div className="stats-summary">
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-label">总交易数</div>
|
||||||
|
<div className="stat-value">{stats.total_trades}</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#999', marginTop: '4px' }}>
|
||||||
|
{stats.meaningful_trades !== undefined && (
|
||||||
|
<>(有意义: {stats.meaningful_trades},0盈亏: {stats.zero_pnl_trades || 0})</>
|
||||||
|
)}
|
||||||
|
{stats.meaningful_trades === undefined && <>(已平仓的完整交易)</>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-label">胜率</div>
|
||||||
|
<div className="stat-value">{stats.win_rate.toFixed(2)}%</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#999', marginTop: '4px' }}>
|
||||||
|
{stats.meaningful_trades !== undefined && <>(已排除0盈亏订单)</>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-label">总盈亏</div>
|
||||||
|
<div className={`stat-value ${stats.total_pnl >= 0 ? 'positive' : 'negative'}`}>
|
||||||
|
{stats.total_pnl.toFixed(2)} USDT
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-label">平均盈亏</div>
|
||||||
|
<div className={`stat-value ${stats.avg_pnl >= 0 ? 'positive' : 'negative'}`}>
|
||||||
|
{stats.avg_pnl.toFixed(2)} USDT
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{"avg_duration_minutes" in stats && stats.avg_duration_minutes !== null && stats.avg_duration_minutes !== undefined && (
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-label">平均持仓时长(分钟)</div>
|
||||||
|
<div className="stat-value">{Number(stats.avg_duration_minutes || 0).toFixed(0)}</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#999', marginTop: '4px' }}>
|
||||||
|
(仅统计“有意义交易”;优先使用 duration_minutes,缺失时用 exit_time-entry_time 计算)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{"exit_reason_counts" in stats && stats.exit_reason_counts && (
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-label">平仓原因(有意义交易)</div>
|
||||||
|
<div className="stat-value" style={{ fontSize: '1.1rem' }}>
|
||||||
|
{(() => {
|
||||||
|
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(' / ') : '—'
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{"avg_win_pnl" in stats && "avg_loss_pnl_abs" in stats && Number(stats.total_pnl || 0) > 0 && (
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-label">平均盈利 / 平均亏损(期望 3:1)</div>
|
||||||
|
<div
|
||||||
|
className={`stat-value ${typeof stats.avg_win_loss_ratio === 'number' && stats.avg_win_loss_ratio >= 3 ? 'positive' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{typeof stats.avg_win_loss_ratio === 'number'
|
||||||
|
? `${stats.avg_win_loss_ratio.toFixed(2)} : 1`
|
||||||
|
: '—'}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#999', marginTop: '4px' }}>
|
||||||
|
+{Number(stats.avg_win_pnl || 0).toFixed(2)} / -{Number(stats.avg_loss_pnl_abs || 0).toFixed(2)} USDT
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{"total_notional_usdt" in stats && (
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-label">总交易量(名义)</div>
|
||||||
|
<div className="stat-value">{Number(stats.total_notional_usdt || 0).toFixed(2)} USDT</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#999', marginTop: '4px' }}>
|
||||||
|
(口径:入场价×数量)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
trades.length === 0 ? (
|
||||||
|
<div className="no-data">暂无交易记录</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* 桌面端表格 */}
|
||||||
|
<table className="trades-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>交易ID</th>
|
||||||
|
<th>交易对</th>
|
||||||
|
<th>方向</th>
|
||||||
|
<th>数量</th>
|
||||||
|
<th>名义</th>
|
||||||
|
<th>保证金</th>
|
||||||
|
<th>入场价</th>
|
||||||
|
<th>出场价</th>
|
||||||
|
<th>盈亏</th>
|
||||||
|
<th>盈亏比例</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>平仓类型</th>
|
||||||
|
<th>币安订单号</th>
|
||||||
|
<th>入场时间</th>
|
||||||
|
<th>平仓时间</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{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 (
|
||||||
|
<tr key={trade.id}>
|
||||||
|
<td style={{ fontSize: '12px', color: '#999' }}>#{trade.id}</td>
|
||||||
|
<td>{trade.symbol}</td>
|
||||||
|
<td className={trade.side === 'BUY' ? 'buy' : 'sell'}>{trade.side}</td>
|
||||||
|
<td>{parseFloat(trade.quantity).toFixed(4)}</td>
|
||||||
|
<td>{notional >= 0.01 ? notional.toFixed(2) : notional.toFixed(4)} USDT</td>
|
||||||
|
<td>{margin >= 0.01 ? margin.toFixed(2) : margin.toFixed(4)} USDT</td>
|
||||||
|
<td>{parseFloat(trade.entry_price).toFixed(4)}</td>
|
||||||
|
<td>{trade.exit_price ? parseFloat(trade.exit_price).toFixed(4) : '-'}</td>
|
||||||
|
<td className={pnl >= 0 ? 'positive' : 'negative'}>
|
||||||
|
{pnl.toFixed(2)} USDT
|
||||||
|
</td>
|
||||||
|
<td className={pnlPercent >= 0 ? 'positive' : 'negative'}>
|
||||||
|
{pnlPercent >= 0 ? '+' : ''}{pnlPercent.toFixed(2)}%
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className={`status ${trade.status}`}>{trade.status === 'open' ? '持仓中' : trade.status === 'closed' ? '已平仓' : '已取消'}</span>
|
||||||
|
</td>
|
||||||
|
<td>{trade.exit_reason_display || '-'}</td>
|
||||||
|
<td className="order-id" style={{ fontSize: '12px' }}>{formatOrderIds()}</td>
|
||||||
|
<td>{formatTime(trade.entry_time)}</td>
|
||||||
|
<td>{trade.exit_time ? formatTime(trade.exit_time) : '-'}</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{/* 移动端卡片 */}
|
||||||
|
<div className="trades-cards">
|
||||||
|
{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 (
|
||||||
|
<div key={trade.id} className="trade-card">
|
||||||
|
<div className="trade-card-header">
|
||||||
|
<span className="trade-card-symbol">{trade.symbol}</span>
|
||||||
|
<span style={{ fontSize: '11px', color: '#999', marginLeft: '8px' }}>交易ID: #{trade.id}</span>
|
||||||
|
<span className={`trade-card-side ${trade.side === 'BUY' ? 'buy' : 'sell'} status ${trade.status}`}>
|
||||||
|
{trade.side === 'BUY' ? '买入' : '卖出'} · {trade.status === 'open' ? '持仓中' : trade.status === 'closed' ? '已平仓' : '已取消'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="trade-card-body">
|
||||||
|
<div className="trade-card-field">
|
||||||
|
<span className="trade-card-label">数量</span>
|
||||||
|
<span className="trade-card-value">{parseFloat(trade.quantity).toFixed(4)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="trade-card-field">
|
||||||
|
<span className="trade-card-label">名义</span>
|
||||||
|
<span className="trade-card-value">{notional >= 0.01 ? notional.toFixed(2) : notional.toFixed(4)} USDT</span>
|
||||||
|
</div>
|
||||||
|
<div className="trade-card-field">
|
||||||
|
<span className="trade-card-label">保证金</span>
|
||||||
|
<span className="trade-card-value">{margin >= 0.01 ? margin.toFixed(2) : margin.toFixed(4)} USDT</span>
|
||||||
|
</div>
|
||||||
|
<div className="trade-card-field">
|
||||||
|
<span className="trade-card-label">入场价</span>
|
||||||
|
<span className="trade-card-value">{parseFloat(trade.entry_price).toFixed(4)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="trade-card-field">
|
||||||
|
<span className="trade-card-label">出场价</span>
|
||||||
|
<span className="trade-card-value">{trade.exit_price ? parseFloat(trade.exit_price).toFixed(4) : '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="trade-card-field">
|
||||||
|
<span className="trade-card-label">盈亏</span>
|
||||||
|
<span className={`trade-card-value ${pnl >= 0 ? 'positive' : 'negative'}`}>
|
||||||
|
{pnl.toFixed(2)} USDT
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="trade-card-field">
|
||||||
|
<span className="trade-card-label">盈亏比例</span>
|
||||||
|
<span className={`trade-card-value ${pnlPercent >= 0 ? 'positive' : 'negative'}`}>
|
||||||
|
{pnlPercent >= 0 ? '+' : ''}{pnlPercent.toFixed(2)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{trade.exit_reason_display && (
|
||||||
|
<div className="trade-card-field">
|
||||||
|
<span className="trade-card-label">平仓类型</span>
|
||||||
|
<span className="trade-card-value">{trade.exit_reason_display}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(trade.entry_order_id || trade.exit_order_id) && (
|
||||||
|
<div className="trade-card-field">
|
||||||
|
<span className="trade-card-label">币安订单号</span>
|
||||||
|
<span className="trade-card-value order-id" style={{ fontSize: '12px' }}>
|
||||||
|
{trade.entry_order_id ? `开仓: ${trade.entry_order_id}` : ''}
|
||||||
|
{trade.entry_order_id && trade.exit_order_id ? ' / ' : ''}
|
||||||
|
{trade.exit_order_id ? `平仓: ${trade.exit_order_id}` : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="trade-card-footer">
|
||||||
|
<div className="trade-time-item">
|
||||||
|
<span className="time-label">入场:</span>
|
||||||
|
<span>{formatTime(trade.entry_time)}</span>
|
||||||
|
</div>
|
||||||
|
{trade.exit_time && (
|
||||||
|
<div className="trade-time-item">
|
||||||
|
<span className="time-label">平仓:</span>
|
||||||
|
<span>{formatTime(trade.exit_time)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div >
|
</div >
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user