From c713e7d27efd6f7404f91455afafab3e3f37fcae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Tue, 3 Feb 2026 13:41:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AE=A2=E5=8D=95=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E7=9A=84=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/TradeList.jsx | 329 +++++++++++++++----------- 1 file changed, 190 insertions(+), 139 deletions(-) diff --git a/frontend/src/components/TradeList.jsx b/frontend/src/components/TradeList.jsx index 88f4d78..9fdf20f 100644 --- a/frontend/src/components/TradeList.jsx +++ b/frontend/src/components/TradeList.jsx @@ -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 = () => { 重置 {trades.length > 0 && ( - )} @@ -344,38 +382,51 @@ const TradeList = () => { { 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} 分钟
平仓原因分布 +
+ {/* 卡片式统计(始终显示) */} +
+
+
总交易数
+
{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) @@ -393,110 +444,110 @@ const TradeList = () => { 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
-
- ) - } + + + )} + {"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
+
+ (口径:入场价×数量) +
+
+ )} + - { - 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
-
- (口径:入场价×数量) + + {/* 详细统计表格(可折叠) */} + {showStatsTable && ( +
+
+

详细统计数据

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
总交易数{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
)}