优化订单记录的页面

This commit is contained in:
薇薇安 2026-02-03 13:41:45 +08:00
parent 8bc6c1ecc4
commit c713e7d27e

View File

@ -9,6 +9,7 @@ const TradeList = () => {
const [trades, setTrades] = useState([])
const [stats, setStats] = useState(null)
const [loading, setLoading] = useState(true)
const [showStatsTable, setShowStatsTable] = useState(false) //
//
const [period, setPeriod] = useState('today') // '1d', '7d', '30d', 'today', 'week', 'month', null
@ -136,13 +137,50 @@ const TradeList = () => {
return row
})
// CSV helper
const convertToCSV = (objArray) => {
const array = typeof objArray !== 'object' ? JSON.parse(objArray) : objArray;
let str = '';
if (array.length === 0) return '';
// Header
const headers = Object.keys(array[0]);
str += headers.join(',') + '\r\n';
// Rows
for (let i = 0; i < array.length; i++) {
let line = '';
for (const index in array[i]) {
if (line !== '') line += ',';
let value = array[i][index];
if (value === null || value === undefined) {
value = '';
} else {
value = String(value);
}
// Escape quotes and wrap in quotes if necessary
// Excel needs double quotes to be escaped as ""
if (value.search(/("|,|\n|\r)/g) >= 0) {
value = '"' + value.replace(/"/g, '""') + '"';
}
line += value;
}
str += line + '\r\n';
}
return str;
}
//
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-')
const filename = `交易记录_${timestamp}.json`
const filename = `交易记录_${timestamp}.csv`
//
const dataStr = JSON.stringify(exportData, null, 2)
const dataBlob = new Blob([dataStr], { type: 'application/json' })
// (CSV with BOM for Excel)
const csvStr = convertToCSV(exportData)
const bom = '\uFEFF'
const dataBlob = new Blob([bom + csvStr], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(dataBlob)
const link = document.createElement('a')
link.href = url
@ -335,8 +373,8 @@ const TradeList = () => {
重置
</button>
{trades.length > 0 && (
<button className="btn-export" onClick={handleExport} title="导出完整数据(含入场/离场原因、入场思路等),便于后续分析">
导出数据 ({trades.length})
<button className="btn-export" onClick={handleExport} title="导出Excel/CSV(含入场/离场原因、入场思路等),便于后续分析">
导出 Excel ({trades.length})
</button>
)}
</div>
@ -344,38 +382,51 @@ const TradeList = () => {
{
stats && (
<div className="stats-copy-section">
<div className="stats-header">
<h3>整体统计分析用</h3>
<button className="btn-copy" onClick={() => handleCopyStats(stats)}>
复制统计
</button>
</div>
<table className="stats-table">
<tbody>
<tr>
<td>总交易数</td>
<td>{stats.total_trades}</td>
</tr>
<tr>
<td>胜率</td>
<td>{stats.win_rate.toFixed(2)}%</td>
</tr>
<tr>
<td>总盈亏</td>
<td>{stats.total_pnl.toFixed(2)} USDT</td>
</tr>
<tr>
<td>平均盈亏</td>
<td>{stats.avg_pnl.toFixed(2)} USDT</td>
</tr>
<tr>
<td>平均持仓时长</td>
<td>{stats.avg_duration_minutes ? Number(stats.avg_duration_minutes).toFixed(0) : 0} 分钟</td>
</tr>
<tr>
<td>平仓原因分布</td>
<td>
<div className="stats-section">
{/* 卡片式统计(始终显示) */}
<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)
@ -393,110 +444,110 @@ const TradeList = () => {
if (other) parts.push(`其他 ${other}`)
return parts.length ? parts.join(' / ') : '—'
})()}
</td>
</tr>
<tr>
<td>盈亏比 (期望 3:1)</td>
<td>{Number(stats.avg_win_loss_ratio || 0).toFixed(2)} : 1</td>
</tr>
<tr>
<td>总交易量 (名义)</td>
<td>{Number(stats.total_notional_usdt || 0).toFixed(2)} USDT</td>
</tr>
</tbody>
</table>
</div>
)
}
</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>
{
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 className="stats-toggle-row">
<button
className="btn-toggle"
onClick={() => setShowStatsTable(!showStatsTable)}
title="查看可复制的详细统计表格"
>
{showStatsTable ? '▲ 收起详细统计' : '▼ 展开详细统计(分析用)'}
</button>
</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' }}>
口径入场价×数量
{/* 详细统计表格(可折叠) */}
{showStatsTable && (
<div className="stats-copy-section">
<div className="stats-header">
<h3>详细统计数据</h3>
<button className="btn-copy" onClick={() => handleCopyStats(stats)}>
复制统计
</button>
</div>
<table className="stats-table">
<tbody>
<tr>
<td>总交易数</td>
<td>{stats.total_trades}</td>
</tr>
<tr>
<td>胜率</td>
<td>{stats.win_rate.toFixed(2)}%</td>
</tr>
<tr>
<td>总盈亏</td>
<td>{stats.total_pnl.toFixed(2)} USDT</td>
</tr>
<tr>
<td>平均盈亏</td>
<td>{stats.avg_pnl.toFixed(2)} USDT</td>
</tr>
<tr>
<td>平均持仓时长</td>
<td>{stats.avg_duration_minutes ? Number(stats.avg_duration_minutes).toFixed(0) : 0} 分钟</td>
</tr>
<tr>
<td>平仓原因分布</td>
<td>
{(() => {
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(' / ') : '—'
})()}
</td>
</tr>
<tr>
<td>盈亏比 (期望 3:1)</td>
<td>{Number(stats.avg_win_loss_ratio || 0).toFixed(2)} : 1</td>
</tr>
<tr>
<td>总交易量 (名义)</td>
<td>{Number(stats.total_notional_usdt || 0).toFixed(2)} USDT</td>
</tr>
</tbody>
</table>
</div>
)}
</div>