a
This commit is contained in:
parent
c23db4aba0
commit
8bc6c1ecc4
|
|
@ -200,14 +200,15 @@
|
|||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
background: #d5d5d5;
|
||||
}
|
||||
|
||||
/* 导出按钮样式 */
|
||||
.btn-export {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
|
|
@ -216,10 +217,9 @@
|
|||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
touch-action: manipulation;
|
||||
min-height: 44px;
|
||||
background: #28a745;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
|
|
@ -230,329 +230,192 @@
|
|||
}
|
||||
|
||||
.btn-export:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #999;
|
||||
font-size: 1.1rem;
|
||||
background: #43A047;
|
||||
}
|
||||
|
||||
.stats-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.stats-summary {
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #f8f9fa;
|
||||
padding: 1.5rem;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.stat-value.positive {
|
||||
color: #27ae60;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.stat-value.negative {
|
||||
color: #e74c3c;
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
/* 表格横向滚动:避免整页过宽,内容区域可左右滑动 */
|
||||
.table-wrapper {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
margin-top: 1rem;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.trades-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
display: none;
|
||||
min-width: 1100px; /* 表格最小宽度,超出时由 table-wrapper 横向滚动 */
|
||||
font-size: 0.9rem;
|
||||
min-width: 1000px; /* 保证在小屏幕上也能横向滚动 */
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.trades-table {
|
||||
display: table;
|
||||
}
|
||||
.trades-table th,
|
||||
.trades-table td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.trades-table th {
|
||||
background-color: #34495e;
|
||||
color: white;
|
||||
padding: 0.5rem 0.4rem;
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
font-size: 0.8rem;
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #444;
|
||||
white-space: nowrap;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.trades-table td {
|
||||
padding: 0.5rem 0.4rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 列宽适度收紧,减少横向占用,仍保证可读 */
|
||||
.trades-table th:nth-child(1),
|
||||
.trades-table td:nth-child(1) {
|
||||
min-width: 52px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(2),
|
||||
.trades-table td:nth-child(2) {
|
||||
min-width: 88px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(3),
|
||||
.trades-table td:nth-child(3) {
|
||||
min-width: 52px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(4),
|
||||
.trades-table td:nth-child(4) {
|
||||
min-width: 78px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(5),
|
||||
.trades-table td:nth-child(5) {
|
||||
min-width: 82px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(6),
|
||||
.trades-table td:nth-child(6) {
|
||||
min-width: 82px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(7),
|
||||
.trades-table td:nth-child(7) {
|
||||
min-width: 78px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(8),
|
||||
.trades-table td:nth-child(8) {
|
||||
min-width: 78px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(9),
|
||||
.trades-table td:nth-child(9) {
|
||||
min-width: 88px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(10),
|
||||
.trades-table td:nth-child(10) {
|
||||
min-width: 88px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(11),
|
||||
.trades-table td:nth-child(11) {
|
||||
min-width: 72px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(12),
|
||||
.trades-table td:nth-child(12) {
|
||||
min-width: 88px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(13),
|
||||
.trades-table td:nth-child(13) {
|
||||
min-width: 160px;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(14),
|
||||
.trades-table td:nth-child(14) {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(15),
|
||||
.trades-table td:nth-child(15) {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.trades-table tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* 移动端卡片式布局 */
|
||||
.trades-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
.pnl-positive {
|
||||
color: #4CAF50;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.trades-cards {
|
||||
display: none;
|
||||
}
|
||||
.pnl-negative {
|
||||
color: #F44336;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.trade-card {
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-open {
|
||||
background: #E3F2FD;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.status-closed {
|
||||
background: #E8F5E9;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
background: #FFEBEE;
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #999;
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 统计复制区域优化 */
|
||||
.stats-copy-section {
|
||||
background: #f1f3f5;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stats-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stats-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-copy {
|
||||
padding: 0.4rem 0.8rem;
|
||||
background: #fff;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
color: #495057;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-copy:hover {
|
||||
background: #e9ecef;
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
|
||||
.stats-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.trade-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
.stats-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid #f1f3f5;
|
||||
font-size: 0.95rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.trade-card-symbol {
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
color: #2c3e50;
|
||||
.stats-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.trade-card-side {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85rem;
|
||||
.stats-table td:first-child {
|
||||
width: 200px;
|
||||
background: #f8f9fa;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
border-right: 1px solid #f1f3f5;
|
||||
}
|
||||
|
||||
.trade-card-side.buy {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.trade-card-side.sell {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.trade-card-body {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.trade-card-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.trade-card-label {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.trade-card-value {
|
||||
font-weight: 500;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.order-id {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.trade-card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid #dee2e6;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.buy {
|
||||
color: #27ae60;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sell {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.positive {
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
.negative {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status.open {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status.closed {
|
||||
background-color: #95a5a6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status.cancelled {
|
||||
background-color: #e74c3c;
|
||||
color: white;
|
||||
.stats-table td:nth-child(2) {
|
||||
font-family: 'SF Mono', 'Roboto Mono', monospace; /* Monospace for numbers alignment if needed */
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,6 +153,40 @@ const TradeList = () => {
|
|||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
// 复制统计数据到剪贴板
|
||||
const handleCopyStats = (statsData) => {
|
||||
if (!statsData) return
|
||||
|
||||
const m = statsData.exit_reason_counts || {}
|
||||
const exitReasonText = [
|
||||
m.stop_loss ? `止损 ${m.stop_loss}` : '',
|
||||
m.take_profit ? `止盈 ${m.take_profit}` : '',
|
||||
m.trailing_stop ? `移动止损 ${m.trailing_stop}` : '',
|
||||
m.manual ? `手动 ${m.manual}` : '',
|
||||
m.sync ? `同步 ${m.sync}` : '',
|
||||
m.unknown ? `其他 ${m.unknown}` : ''
|
||||
].filter(Boolean).join(' / ') || '—'
|
||||
|
||||
const lines = [
|
||||
`总交易数\t${statsData.total_trades}`,
|
||||
`胜率\t${statsData.win_rate.toFixed(2)}%`,
|
||||
`总盈亏\t${statsData.total_pnl.toFixed(2)} USDT`,
|
||||
`平均盈亏\t${statsData.avg_pnl.toFixed(2)} USDT`,
|
||||
`平均持仓时长\t${statsData.avg_duration_minutes ? Number(statsData.avg_duration_minutes).toFixed(0) : 0} 分钟`,
|
||||
`平仓原因分布\t${exitReasonText}`,
|
||||
`盈亏比\t${Number(statsData.avg_win_loss_ratio || 0).toFixed(2)} : 1`,
|
||||
`总交易量\t${Number(statsData.total_notional_usdt || 0).toFixed(2)} USDT`
|
||||
]
|
||||
|
||||
const text = lines.join('\n')
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
alert('统计数据已复制到剪贴板(可直接粘贴到Excel)')
|
||||
}).catch(err => {
|
||||
console.error('Copy failed:', err)
|
||||
alert('复制失败,请手动复制')
|
||||
})
|
||||
}
|
||||
|
||||
if (loading) return <div className="loading">加载中...</div>
|
||||
|
||||
return (
|
||||
|
|
@ -310,36 +344,67 @@ const TradeList = () => {
|
|||
|
||||
{
|
||||
stats && (
|
||||
<div >
|
||||
<div style={{ fontSize: '1.1rem' }}>整体统计</div>
|
||||
<div style={{ fontSize: '1.1rem' }}>总交易数:{stats.total_trades} </div>
|
||||
<div style={{ fontSize: '1.1rem' }}>胜率:{stats.win_rate.toFixed(2)}%</div>
|
||||
<div style={{ fontSize: '1.1rem' }}>总盈亏:{stats.total_pnl.toFixed(2)} USDT</div>
|
||||
<div style={{ fontSize: '1.1rem' }}>平均盈亏:{stats.avg_pnl.toFixed(2)} USDT</div>
|
||||
<div style={{ fontSize: '1.1rem' }}>平均持仓时长(分钟):{stats.avg_duration_minutes ? Number(stats.avg_duration_minutes).toFixed(0) : 0}</div>
|
||||
<div style={{ fontSize: '1.1rem' }}>平仓原因(有意义交易):
|
||||
<div 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 style={{ fontSize: '1.1rem' }}>平均盈利 / 平均亏损(期望 3:1):{Number(stats.avg_win_loss_ratio || 0).toFixed(2)} : 1</div>
|
||||
<div style={{ fontSize: '1.1rem' }}>总交易量(名义):{Number(stats.total_notional_usdt || 0).toFixed(2)} USDT</div>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user