diff --git a/frontend/src/components/TradeList.jsx b/frontend/src/components/TradeList.jsx index 9f82e55..74ae429 100644 --- a/frontend/src/components/TradeList.jsx +++ b/frontend/src/components/TradeList.jsx @@ -243,79 +243,156 @@ const TradeList = () => { 交易对 方向 数量 + 保证金 入场价 出场价 盈亏 + 盈亏比例 状态 平仓类型 时间 - {trades.map(trade => ( - - {trade.symbol} - {trade.side} - {parseFloat(trade.quantity).toFixed(4)} - {parseFloat(trade.entry_price).toFixed(4)} - {trade.exit_price ? parseFloat(trade.exit_price).toFixed(4) : '-'} - = 0 ? 'positive' : 'negative'}> - {parseFloat(trade.pnl).toFixed(2)} USDT - - - {trade.status === 'open' ? '持仓中' : trade.status === 'closed' ? '已平仓' : '已取消'} - - {trade.exit_reason_display || '-'} - {new Date(trade.entry_time).toLocaleString('zh-CN')} - - ))} + {trades.map(trade => { + // 计算保证金:如果有entry_value_usdt和leverage,则计算;否则使用名义价值/杠杆 + const entryValue = trade.entry_value_usdt !== undefined + ? parseFloat(trade.entry_value_usdt) + : (parseFloat(trade.quantity || 0) * parseFloat(trade.entry_price || 0)) + const leverage = parseFloat(trade.leverage || 10) + const margin = leverage > 0 ? entryValue / leverage : 0 + + // 计算盈亏比例(盈亏/保证金) + const pnl = parseFloat(trade.pnl || 0) + const pnlPercent = margin > 0 ? (pnl / margin) * 100 : 0 + + // 格式化时间为北京时间 + const formatTime = (timeStr) => { + if (!timeStr) return '-' + try { + const date = new Date(timeStr) + if (isNaN(date.getTime())) return timeStr + 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 timeStr + } + } + + return ( + + {trade.symbol} + {trade.side} + {parseFloat(trade.quantity).toFixed(4)} + {margin.toFixed(2)} 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 || '-'} + {formatTime(trade.entry_time)} + + ) + })} {/* 移动端卡片 */}
- {trades.map(trade => ( -
-
- {trade.symbol} - - {trade.side === 'BUY' ? '买入' : '卖出'} · {trade.status === 'open' ? '持仓中' : trade.status === 'closed' ? '已平仓' : '已取消'} - -
-
-
- 数量 - {parseFloat(trade.quantity).toFixed(4)} -
-
- 入场价 - {parseFloat(trade.entry_price).toFixed(4)} -
-
- 出场价 - {trade.exit_price ? parseFloat(trade.exit_price).toFixed(4) : '-'} -
-
- 盈亏 - = 0 ? 'positive' : 'negative'}`}> - {parseFloat(trade.pnl).toFixed(2)} USDT + {trades.map(trade => { + // 计算保证金和盈亏比例 + const entryValue = trade.entry_value_usdt !== undefined + ? parseFloat(trade.entry_value_usdt) + : (parseFloat(trade.quantity || 0) * parseFloat(trade.entry_price || 0)) + const leverage = parseFloat(trade.leverage || 10) + const margin = leverage > 0 ? entryValue / leverage : 0 + const pnl = parseFloat(trade.pnl || 0) + const pnlPercent = margin > 0 ? (pnl / margin) * 100 : 0 + + // 格式化时间为北京时间 + const formatTime = (timeStr) => { + if (!timeStr) return '-' + try { + const date = new Date(timeStr) + if (isNaN(date.getTime())) return timeStr + return date.toLocaleString('zh-CN', { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + timeZone: 'Asia/Shanghai' + }) + } catch (e) { + return timeStr + } + } + + return ( +
+
+ {trade.symbol} + + {trade.side === 'BUY' ? '买入' : '卖出'} · {trade.status === 'open' ? '持仓中' : trade.status === 'closed' ? '已平仓' : '已取消'}
- {trade.exit_reason_display && ( +
- 平仓类型 - {trade.exit_reason_display} + 数量 + {parseFloat(trade.quantity).toFixed(4)}
- )} +
+ 保证金 + {margin.toFixed(2)} 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} +
+ )} +
+
+ {formatTime(trade.entry_time)} + {trade.exit_time && ( + 平仓: {formatTime(trade.exit_time)} + )} +
-
- {new Date(trade.entry_time).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })} - {trade.exit_time && ( - 平仓: {new Date(trade.exit_time).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })} - )} -
-
- ))} + ) + })}
)} diff --git a/recommendations-viewer/src/components/RecommendationsViewer.jsx b/recommendations-viewer/src/components/RecommendationsViewer.jsx index 433a690..fef5e2b 100644 --- a/recommendations-viewer/src/components/RecommendationsViewer.jsx +++ b/recommendations-viewer/src/components/RecommendationsViewer.jsx @@ -104,6 +104,24 @@ function RecommendationsViewer() { }; const formatTime = (timeStr) => { + if (!timeStr) return '-' + try { + const date = new Date(timeStr) + if (isNaN(date.getTime())) return timeStr + // 使用北京时间格式化 + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + timeZone: 'Asia/Shanghai' // 明确使用北京时间 + }) + } catch (e) { + return timeStr + } + } if (!timeStr) return '-'; try { const date = new Date(timeStr);