2291 lines
84 KiB
HTML
2291 lines
84 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<title>Tradus Auto Trading | Based on AI Agent</title>
|
||
<link rel="stylesheet" href="/static/css/style.css">
|
||
<style>
|
||
/* 防止横向滚动 */
|
||
html, body {
|
||
overflow-x: hidden;
|
||
max-width: 100vw;
|
||
}
|
||
|
||
/* 覆盖全局 #app 样式 */
|
||
#app {
|
||
height: auto;
|
||
display: block;
|
||
align-items: initial;
|
||
justify-content: initial;
|
||
padding: 0;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
.trading-page {
|
||
min-height: 100vh;
|
||
background: var(--bg-primary);
|
||
padding: 20px;
|
||
}
|
||
|
||
.trading-container {
|
||
max-width: 1400px;
|
||
min-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
/* 固定顶部区域 */
|
||
.sticky-header {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
background: var(--bg-primary);
|
||
padding-bottom: 10px;
|
||
}
|
||
|
||
.trading-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
padding: 10px 0 20px 0;
|
||
border-bottom: 1px solid var(--border);
|
||
background: var(--bg-primary);
|
||
}
|
||
|
||
.trading-title {
|
||
font-size: 24px;
|
||
font-weight: 300;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.trading-title span {
|
||
color: var(--accent);
|
||
}
|
||
|
||
.refresh-btn {
|
||
padding: 8px 16px;
|
||
background: transparent;
|
||
border: 1px solid var(--accent);
|
||
color: var(--accent);
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.refresh-btn:hover {
|
||
background: var(--accent);
|
||
color: var(--bg-primary);
|
||
}
|
||
|
||
/* 发送报告按钮 */
|
||
.report-btn {
|
||
padding: 8px 16px;
|
||
background: transparent;
|
||
border: 1px solid #1da1f2;
|
||
color: #1da1f2;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.report-btn:hover {
|
||
background: rgba(29, 161, 242, 0.1);
|
||
}
|
||
|
||
.report-btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* 统计卡片 */
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||
gap: 16px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.stat-card {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border);
|
||
border-radius: 4px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 24px;
|
||
font-weight: 300;
|
||
color: var(--accent);
|
||
}
|
||
|
||
.stat-value.positive {
|
||
color: #00ff41;
|
||
}
|
||
|
||
.stat-value.negative {
|
||
color: #ff4444;
|
||
}
|
||
|
||
/* 标签页 */
|
||
.tabs {
|
||
display: flex;
|
||
gap: 0;
|
||
margin-bottom: 20px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.tab {
|
||
padding: 12px 24px;
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--text-secondary);
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
border-bottom: 2px solid transparent;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.tab:hover {
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.tab.active {
|
||
color: var(--accent);
|
||
border-bottom-color: var(--accent);
|
||
}
|
||
|
||
/* 表格 */
|
||
.orders-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.orders-table th,
|
||
.orders-table td {
|
||
padding: 12px 16px;
|
||
text-align: left;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.orders-table th {
|
||
background: var(--bg-primary);
|
||
color: var(--text-secondary);
|
||
font-weight: 400;
|
||
font-size: 12px;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.orders-table td {
|
||
color: var(--text-primary);
|
||
font-size: 14px;
|
||
}
|
||
|
||
.orders-table tr:hover {
|
||
background: var(--bg-tertiary);
|
||
}
|
||
|
||
/* 状态标签 */
|
||
.status-badge {
|
||
display: inline-block;
|
||
padding: 4px 8px;
|
||
border-radius: 2px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.status-badge.pending {
|
||
background: rgba(255, 165, 0, 0.1);
|
||
color: orange;
|
||
}
|
||
|
||
.status-badge.open {
|
||
background: rgba(0, 255, 65, 0.1);
|
||
color: #00ff41;
|
||
}
|
||
|
||
.status-badge.closed_tp {
|
||
background: rgba(0, 255, 65, 0.1);
|
||
color: #00ff41;
|
||
}
|
||
|
||
.status-badge.closed_sl {
|
||
background: rgba(255, 68, 68, 0.1);
|
||
color: #ff4444;
|
||
}
|
||
|
||
.status-badge.closed_be {
|
||
background: rgba(255, 193, 7, 0.1);
|
||
color: #ffc107;
|
||
}
|
||
|
||
.status-badge.closed_manual {
|
||
background: rgba(255, 165, 0, 0.1);
|
||
color: orange;
|
||
}
|
||
|
||
/* 方向标签 */
|
||
.side-badge {
|
||
display: inline-block;
|
||
padding: 4px 8px;
|
||
border-radius: 2px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.side-badge.long {
|
||
background: rgba(0, 255, 65, 0.1);
|
||
color: #00ff41;
|
||
}
|
||
|
||
.side-badge.short {
|
||
background: rgba(255, 68, 68, 0.1);
|
||
color: #ff4444;
|
||
}
|
||
|
||
/* 等级标签 */
|
||
.grade-badge {
|
||
display: inline-block;
|
||
padding: 2px 6px;
|
||
border-radius: 2px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.grade-badge.A {
|
||
background: rgba(255, 215, 0, 0.2);
|
||
color: gold;
|
||
}
|
||
|
||
.grade-badge.B {
|
||
background: rgba(192, 192, 192, 0.2);
|
||
color: silver;
|
||
}
|
||
|
||
.grade-badge.C {
|
||
background: rgba(205, 127, 50, 0.2);
|
||
color: #cd7f32;
|
||
}
|
||
|
||
/* 盈亏显示 */
|
||
.pnl {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.pnl.positive {
|
||
color: #00ff41;
|
||
}
|
||
|
||
.pnl.negative {
|
||
color: #ff4444;
|
||
}
|
||
|
||
/* 按等级统计 */
|
||
.grade-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 16px;
|
||
margin-top: 30px;
|
||
}
|
||
|
||
.grade-card {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border);
|
||
border-radius: 4px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.grade-card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.grade-card-title {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.grade-card-stats {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.grade-stat-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.grade-stat-label {
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* 空状态 */
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 60px 20px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.empty-state svg {
|
||
width: 48px;
|
||
height: 48px;
|
||
margin-bottom: 16px;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
/* 监控状态 */
|
||
.monitor-status {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.monitor-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: #ff4444;
|
||
}
|
||
|
||
.monitor-dot.running {
|
||
background: #00ff41;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.5; }
|
||
}
|
||
|
||
/* 价格显示 */
|
||
.price-list {
|
||
display: flex;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.price-item {
|
||
background: var(--bg-tertiary);
|
||
padding: 8px 12px;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.price-item .symbol {
|
||
color: var(--text-secondary);
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.price-item .price {
|
||
color: var(--accent);
|
||
font-family: monospace;
|
||
}
|
||
|
||
/* 加载状态 */
|
||
.loading {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* 重置按钮 */
|
||
.reset-btn {
|
||
padding: 8px 16px;
|
||
background: transparent;
|
||
border: 1px solid #ff4444;
|
||
color: #ff4444;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.reset-btn:hover {
|
||
background: rgba(255, 68, 68, 0.1);
|
||
}
|
||
|
||
/* 管理下拉菜单 */
|
||
.admin-dropdown {
|
||
position: relative;
|
||
}
|
||
|
||
.admin-btn {
|
||
padding: 8px 16px;
|
||
background: transparent;
|
||
border: 1px solid var(--border-bright);
|
||
color: var(--text-secondary);
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.admin-btn:hover {
|
||
border-color: var(--accent);
|
||
color: var(--accent);
|
||
}
|
||
|
||
.admin-menu {
|
||
position: absolute;
|
||
top: 100%;
|
||
right: 0;
|
||
margin-top: 4px;
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border);
|
||
border-radius: 4px;
|
||
min-width: 120px;
|
||
z-index: 1000;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.admin-menu button {
|
||
display: block;
|
||
width: 100%;
|
||
padding: 10px 16px;
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--text-primary);
|
||
font-size: 14px;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.admin-menu button:hover {
|
||
background: var(--bg-tertiary);
|
||
}
|
||
|
||
.admin-menu button.danger {
|
||
color: #ff4444;
|
||
}
|
||
|
||
.admin-menu button.danger:hover {
|
||
background: rgba(255, 68, 68, 0.1);
|
||
}
|
||
|
||
/* 操作按钮 */
|
||
.action-btn {
|
||
padding: 4px 8px;
|
||
background: transparent;
|
||
border: 1px solid var(--border-bright);
|
||
color: var(--text-secondary);
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.action-btn:hover {
|
||
border-color: var(--accent);
|
||
color: var(--accent);
|
||
}
|
||
|
||
.action-btn.danger:hover {
|
||
border-color: #ff4444;
|
||
color: #ff4444;
|
||
}
|
||
|
||
/* 持仓汇总 */
|
||
.position-summary {
|
||
display: flex;
|
||
gap: 24px;
|
||
padding: 16px 20px;
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border);
|
||
border-radius: 4px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.summary-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.summary-label {
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.summary-value {
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.summary-value.positive {
|
||
color: #00ff41;
|
||
}
|
||
|
||
.summary-value.negative {
|
||
color: #ff4444;
|
||
}
|
||
|
||
/* 现价样式 */
|
||
.current-price {
|
||
font-family: monospace;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.current-price.price-up {
|
||
color: #00ff41;
|
||
}
|
||
|
||
.current-price.price-down {
|
||
color: #ff4444;
|
||
}
|
||
|
||
/* 警告价格(接近止损) */
|
||
.warning-price {
|
||
color: #ff4444;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 成功价格(接近止盈) */
|
||
.success-price {
|
||
color: #00ff41;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 盈亏小字 */
|
||
.pnl small {
|
||
font-size: 11px;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.trading-page {
|
||
padding: 10px;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
.trading-container {
|
||
min-width: auto;
|
||
max-width: 100%;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
/* 取消顶部固定 */
|
||
.sticky-header {
|
||
position: static;
|
||
}
|
||
|
||
.trading-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
}
|
||
|
||
.trading-title {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.trading-title span {
|
||
display: block;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.stats-grid {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 10px;
|
||
}
|
||
|
||
.stat-card {
|
||
padding: 12px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 16px;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.tabs {
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
.tab {
|
||
padding: 10px 16px;
|
||
font-size: 13px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 表格横向滚动 */
|
||
.table-wrapper {
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
.orders-table {
|
||
min-width: 800px;
|
||
}
|
||
|
||
.orders-table th,
|
||
.orders-table td {
|
||
padding: 10px 12px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.position-summary {
|
||
flex-wrap: wrap;
|
||
gap: 16px;
|
||
}
|
||
|
||
.summary-item {
|
||
min-width: calc(50% - 8px);
|
||
}
|
||
|
||
.grade-stats {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.monitor-status span {
|
||
display: none;
|
||
}
|
||
|
||
/* 实时价格 */
|
||
.price-list {
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.price-item {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.stats-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.trading-title {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
/* 分享链接 */
|
||
.action-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.share-link {
|
||
color: var(--text-secondary);
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
opacity: 0.6;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.share-link:hover {
|
||
color: var(--accent);
|
||
opacity: 1;
|
||
}
|
||
|
||
/* 分享模态框 */
|
||
.share-modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.8);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.share-modal {
|
||
background: var(--bg-secondary);
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
max-width: 400px;
|
||
width: 90%;
|
||
}
|
||
|
||
.share-modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.share-modal-header h3 {
|
||
margin: 0;
|
||
color: var(--text-primary);
|
||
font-weight: 400;
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-secondary);
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
/* 分享卡片 */
|
||
.share-card {
|
||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
color: #fff;
|
||
}
|
||
|
||
.share-card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.share-logo {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
letter-spacing: 2px;
|
||
color: var(--accent);
|
||
}
|
||
|
||
.share-time {
|
||
font-size: 12px;
|
||
color: rgba(255, 255, 255, 0.6);
|
||
}
|
||
|
||
.share-card-body {
|
||
text-align: center;
|
||
}
|
||
|
||
.share-symbol {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.symbol-name {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.side-tag {
|
||
padding: 4px 12px;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.side-tag.long {
|
||
background: rgba(0, 200, 150, 0.2);
|
||
color: #00c896;
|
||
}
|
||
|
||
.side-tag.short {
|
||
background: rgba(255, 99, 99, 0.2);
|
||
color: #ff6363;
|
||
}
|
||
|
||
.share-pnl {
|
||
font-size: 48px;
|
||
font-weight: 700;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.share-pnl.positive {
|
||
color: #00c896;
|
||
}
|
||
|
||
.share-pnl.negative {
|
||
color: #ff6363;
|
||
}
|
||
|
||
.share-pnl-amount {
|
||
font-size: 20px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.share-pnl-amount.positive {
|
||
color: #00c896;
|
||
}
|
||
|
||
.share-pnl-amount.negative {
|
||
color: #ff6363;
|
||
}
|
||
|
||
.share-prices {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 40px;
|
||
margin-bottom: 24px;
|
||
padding: 16px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.share-prices .price-item {
|
||
text-align: center;
|
||
}
|
||
|
||
.share-prices .price-label {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: rgba(255, 255, 255, 0.6);
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.share-prices .price-value {
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.share-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding-top: 16px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.share-grade {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.share-grade .grade-badge {
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.share-grade .grade-badge.A {
|
||
background: linear-gradient(135deg, #ffd700, #ffb300);
|
||
color: #1a1a2e;
|
||
}
|
||
|
||
.share-grade .grade-badge.B {
|
||
background: linear-gradient(135deg, #c0c0c0, #a0a0a0);
|
||
color: #1a1a2e;
|
||
}
|
||
|
||
.share-grade .grade-badge.C {
|
||
background: linear-gradient(135deg, #cd7f32, #a0522d);
|
||
color: #fff;
|
||
}
|
||
|
||
.share-grade .grade-text {
|
||
font-size: 12px;
|
||
color: rgba(255, 255, 255, 0.6);
|
||
}
|
||
|
||
.share-slogan {
|
||
font-size: 12px;
|
||
color: rgba(255, 255, 255, 0.4);
|
||
font-style: italic;
|
||
}
|
||
|
||
/* 分享按钮 */
|
||
.share-modal-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.share-btn {
|
||
flex: 1;
|
||
padding: 12px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.share-btn.download {
|
||
background: var(--accent);
|
||
color: #1a1a2e;
|
||
}
|
||
|
||
.share-btn.download:hover {
|
||
background: #00b894;
|
||
}
|
||
|
||
.share-btn.copy {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.share-btn.copy:hover {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
/* 收益率图表样式 */
|
||
.chart-container {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border);
|
||
border-radius: 4px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.chart-wrapper {
|
||
position: relative;
|
||
height: 400px;
|
||
width: 100%;
|
||
}
|
||
|
||
.daily-returns-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.daily-returns-table th,
|
||
.daily-returns-table td {
|
||
padding: 12px 16px;
|
||
text-align: center;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.daily-returns-table th {
|
||
background: var(--bg-primary);
|
||
color: var(--text-secondary);
|
||
font-weight: 400;
|
||
font-size: 12px;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.daily-returns-table td {
|
||
color: var(--text-primary);
|
||
font-size: 14px;
|
||
}
|
||
|
||
.daily-returns-table tr:hover {
|
||
background: var(--bg-tertiary);
|
||
}
|
||
|
||
.daily-returns-table .positive {
|
||
color: #00ff41;
|
||
}
|
||
|
||
.daily-returns-table .negative {
|
||
color: #ff4444;
|
||
}
|
||
|
||
.summary-chart-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||
gap: 16px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.summary-chart-card {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border);
|
||
border-radius: 4px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.summary-chart-label {
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.summary-chart-value {
|
||
font-size: 20px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.summary-chart-value.positive {
|
||
color: #00ff41;
|
||
}
|
||
|
||
.summary-chart-value.negative {
|
||
color: #ff4444;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="app">
|
||
<div class="trading-page">
|
||
<div class="trading-container">
|
||
<!-- 固定顶部区域 -->
|
||
<div class="sticky-header">
|
||
<!-- 头部 -->
|
||
<div class="trading-header">
|
||
<h1 class="trading-title">AI 自动化交易 <span>| AI Agent Trading System</span></h1>
|
||
<div style="display: flex; align-items: center; gap: 12px;">
|
||
<div class="monitor-status">
|
||
<div class="monitor-dot" :class="{ running: monitorRunning }"></div>
|
||
<span>{{ monitorRunning ? '监控中' : '未启动' }}</span>
|
||
</div>
|
||
<div class="admin-dropdown">
|
||
<button class="admin-btn" @click="toggleAdminMenu">管理 ▾</button>
|
||
<div class="admin-menu" v-if="showAdminMenu">
|
||
<button @click="adminSendReport">发送报告</button>
|
||
<button @click="adminResetData" class="danger">重置数据</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 统计卡片 -->
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-label">账户余额</div>
|
||
<div class="stat-value" :class="account.current_balance >= account.initial_balance ? 'positive' : 'negative'">
|
||
${{ account.current_balance?.toFixed(2) || '0.00' }}
|
||
</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">收益率</div>
|
||
<div class="stat-value" :class="account.current_balance >= account.initial_balance ? 'positive' : 'negative'">
|
||
{{ account.initial_balance ? (((account.current_balance - account.initial_balance) / account.initial_balance) * 100).toFixed(2) : '0.00' }}%
|
||
</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">收益额</div>
|
||
<div class="stat-value" :class="stats.total_pnl >= 0 ? 'positive' : 'negative'">
|
||
${{ stats.total_pnl.toFixed(2) }}
|
||
</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">交易总数</div>
|
||
<div class="stat-value">{{ stats.total_trades }}</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">胜率</div>
|
||
<div class="stat-value">{{ stats.win_rate.toFixed(1) }}%</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">盈亏比</div>
|
||
<div class="stat-value">{{ stats.profit_factor === Infinity ? '∞' : stats.profit_factor.toFixed(2) }}</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">最大回撤</div>
|
||
<div class="stat-value" :class="stats.max_drawdown <= 0 ? 'negative' : 'positive'">
|
||
{{ stats.max_drawdown ? stats.max_drawdown.toFixed(2) : '0.00' }}%
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 实时价格 -->
|
||
<div v-if="Object.keys(latestPrices).length > 0" style="margin-bottom: 15px;">
|
||
<div class="stat-label" style="margin-bottom: 8px;">实时价格</div>
|
||
<div class="price-list">
|
||
<div class="price-item" v-for="(price, symbol) in latestPrices" :key="symbol">
|
||
<span class="symbol">{{ symbol }}</span>
|
||
<span class="price">${{ price.toLocaleString() }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 标签页 -->
|
||
<div class="tabs">
|
||
<button class="tab" :class="{ active: activeTab === 'active' }" @click="activeTab = 'active'">
|
||
活跃订单 ({{ activeOrders.length }})
|
||
</button>
|
||
<button class="tab" :class="{ active: activeTab === 'history' }" @click="activeTab = 'history'">
|
||
历史订单
|
||
</button>
|
||
<button class="tab" :class="{ active: activeTab === 'stats' }" @click="activeTab = 'stats'">
|
||
详细统计
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 活跃订单 -->
|
||
<div v-if="activeTab === 'active'">
|
||
<!-- 持仓汇总 -->
|
||
<div v-if="openOrdersCount > 0" class="position-summary">
|
||
<div class="summary-item">
|
||
<span class="summary-label">持仓数量</span>
|
||
<span class="summary-value">{{ openOrdersCount }}</span>
|
||
</div>
|
||
<div class="summary-item">
|
||
<span class="summary-label">总仓位</span>
|
||
<span class="summary-value">${{ totalPosition.toFixed(2) }}</span>
|
||
</div>
|
||
<div class="summary-item">
|
||
<span class="summary-label">浮动盈亏</span>
|
||
<span class="summary-value" :class="totalUnrealizedPnl >= 0 ? 'positive' : 'negative'">
|
||
{{ totalUnrealizedPnl >= 0 ? '+' : '' }}${{ totalUnrealizedPnl.toFixed(2) }}
|
||
({{ totalUnrealizedPnlPercent >= 0 ? '+' : '' }}{{ totalUnrealizedPnlPercent.toFixed(2) }}%)
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="loading" class="loading">加载中...</div>
|
||
<div v-else-if="activeOrders.length === 0" class="empty-state">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
|
||
</svg>
|
||
<p>暂无活跃订单</p>
|
||
</div>
|
||
<div v-else class="table-wrapper">
|
||
<table class="orders-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>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="order in activeOrders" :key="order.order_id">
|
||
<td>{{ order.order_id.slice(-12) }}</td>
|
||
<td>{{ order.symbol }}</td>
|
||
<td><span class="side-badge" :class="order.side">{{ order.side === 'long' ? '做多' : '做空' }}</span></td>
|
||
<td>
|
||
<span class="status-badge" :class="order.status">
|
||
{{ order.status === 'pending' ? '⏳ 挂单' : '✅ 持仓' }}
|
||
</span>
|
||
</td>
|
||
<td><span class="grade-badge" :class="order.signal_grade">{{ order.signal_grade }}</span></td>
|
||
<td>${{ order.entry_price?.toLocaleString() }}</td>
|
||
<td>
|
||
<span class="current-price" :class="getPriceChangeClass(order)">
|
||
${{ getCurrentPrice(order.symbol)?.toLocaleString() || '-' }}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<template v-if="order.status === 'open'">
|
||
<span class="pnl" :class="getUnrealizedPnl(order).pnl >= 0 ? 'positive' : 'negative'">
|
||
{{ getUnrealizedPnl(order).pnl >= 0 ? '+' : '' }}{{ getUnrealizedPnl(order).percent.toFixed(2) }}%
|
||
<br>
|
||
<small>(${{ getUnrealizedPnl(order).pnl >= 0 ? '+' : '' }}{{ getUnrealizedPnl(order).pnl.toFixed(2) }})</small>
|
||
</span>
|
||
</template>
|
||
<template v-else>
|
||
<span style="color: var(--text-secondary);">等待入场</span>
|
||
</template>
|
||
</td>
|
||
<td>
|
||
<span :class="isNearStopLoss(order) ? 'warning-price' : ''">
|
||
${{ order.stop_loss?.toLocaleString() }}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<span :class="isNearTakeProfit(order) ? 'success-price' : ''">
|
||
${{ order.take_profit?.toLocaleString() }}
|
||
</span>
|
||
</td>
|
||
<td>${{ order.quantity }}</td>
|
||
<td>{{ formatTime(order.status === 'open' ? order.opened_at : order.created_at) }}</td>
|
||
<td class="action-cell">
|
||
<button class="action-btn danger" @click="closeOrder(order)">
|
||
{{ order.status === 'pending' ? '取消' : '平仓' }}
|
||
</button>
|
||
<span v-if="order.status === 'open'" class="share-link" @click="shareOrder(order)" title="分享">
|
||
分享
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 历史订单 -->
|
||
<div v-if="activeTab === 'history'">
|
||
<div v-if="loading" class="loading">加载中...</div>
|
||
<div v-else-if="historyOrders.length === 0" class="empty-state">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
<path d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||
</svg>
|
||
<p>暂无历史订单</p>
|
||
</div>
|
||
<div v-else class="table-wrapper">
|
||
<table class="orders-table">
|
||
<thead>
|
||
<tr>
|
||
<th>订单ID</th>
|
||
<th>交易对</th>
|
||
<th>方向</th>
|
||
<th>等级</th>
|
||
<th>入场价</th>
|
||
<th>出场价</th>
|
||
<th>盈亏</th>
|
||
<th>状态</th>
|
||
<th>平仓时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="order in historyOrders" :key="order.order_id">
|
||
<td>{{ order.order_id.slice(-12) }}</td>
|
||
<td>{{ order.symbol }}</td>
|
||
<td><span class="side-badge" :class="order.side">{{ order.side === 'long' ? '做多' : '做空' }}</span></td>
|
||
<td><span class="grade-badge" :class="order.signal_grade">{{ order.signal_grade }}</span></td>
|
||
<td>${{ order.filled_price?.toLocaleString() }}</td>
|
||
<td>${{ order.exit_price?.toLocaleString() }}</td>
|
||
<td>
|
||
<span class="pnl" :class="order.pnl_amount >= 0 ? 'positive' : 'negative'">
|
||
{{ order.pnl_percent >= 0 ? '+' : '' }}{{ order.pnl_percent?.toFixed(2) }}%
|
||
(${{ order.pnl_amount >= 0 ? '+' : '' }}{{ order.pnl_amount?.toFixed(2) }})
|
||
</span>
|
||
</td>
|
||
<td><span class="status-badge" :class="order.status">{{ formatStatus(order.status) }}</span></td>
|
||
<td>{{ formatTime(order.closed_at) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 详细统计 -->
|
||
<div v-if="activeTab === 'stats'">
|
||
<!-- 总体指标汇总 -->
|
||
<div class="grade-stats" style="margin-bottom: 30px;">
|
||
<div class="grade-card">
|
||
<div class="grade-card-header">
|
||
<span class="grade-card-title">交易统计</span>
|
||
</div>
|
||
<div class="grade-card-stats">
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">总交易数</span>
|
||
<span>{{ stats.total_trades }}</span>
|
||
</div>
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">盈利交易</span>
|
||
<span class="positive">{{ stats.winning_trades }}</span>
|
||
</div>
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">亏损交易</span>
|
||
<span class="negative">{{ stats.losing_trades }}</span>
|
||
</div>
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">胜率</span>
|
||
<span>{{ stats.win_rate.toFixed(1) }}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="grade-card">
|
||
<div class="grade-card-header">
|
||
<span class="grade-card-title">盈亏指标</span>
|
||
</div>
|
||
<div class="grade-card-stats">
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">盈亏比</span>
|
||
<span>{{ stats.profit_factor === Infinity ? '∞' : stats.profit_factor.toFixed(2) }}</span>
|
||
</div>
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">最佳交易</span>
|
||
<span class="positive">{{ stats.best_trade.toFixed(2) }}%</span>
|
||
</div>
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">最差交易</span>
|
||
<span class="negative">{{ stats.worst_trade.toFixed(2) }}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="grade-card">
|
||
<div class="grade-card-header">
|
||
<span class="grade-card-title">风险指标</span>
|
||
</div>
|
||
<div class="grade-card-stats">
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">最大回撤</span>
|
||
<span :class="stats.max_drawdown <= 0 ? 'negative' : 'positive'">
|
||
{{ stats.max_drawdown ? stats.max_drawdown.toFixed(2) : '0.00' }}%
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="grade-card">
|
||
<div class="grade-card-header">
|
||
<span class="grade-card-title">收益统计</span>
|
||
</div>
|
||
<div class="grade-card-stats">
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">累计收益率</span>
|
||
<span :class="totalReturn >= 0 ? 'positive' : 'negative'">
|
||
{{ totalReturn >= 0 ? '+' : '' }}{{ totalReturn.toFixed(2) }}%
|
||
</span>
|
||
</div>
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">盈利天数</span>
|
||
<span class="positive">{{ profitableDays }}</span>
|
||
</div>
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">亏损天数</span>
|
||
<span class="negative">{{ losingDays }}</span>
|
||
</div>
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">日胜率</span>
|
||
<span>{{ dailyWinRate.toFixed(1) }}%</span>
|
||
</div>
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">平均日收益</span>
|
||
<span :class="avgDailyReturn >= 0 ? 'positive' : 'negative'">
|
||
{{ avgDailyReturn >= 0 ? '+' : '' }}{{ avgDailyReturn.toFixed(2) }}%
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 style="color: var(--text-primary); font-weight: 300; margin-bottom: 16px;">按信号等级统计</h3>
|
||
<div class="grade-stats">
|
||
<div class="grade-card" v-for="(data, grade) in stats.by_grade" :key="grade">
|
||
<div class="grade-card-header">
|
||
<span class="grade-card-title">
|
||
<span class="grade-badge" :class="grade">{{ grade }}</span> 级信号
|
||
</span>
|
||
</div>
|
||
<div class="grade-card-stats">
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">交易数</span>
|
||
<span>{{ data.count }}</span>
|
||
</div>
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">胜率</span>
|
||
<span>{{ data.win_rate.toFixed(1) }}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 style="color: var(--text-primary); font-weight: 300; margin: 30px 0 16px;">按交易对统计</h3>
|
||
<div class="grade-stats">
|
||
<div class="grade-card" v-for="(data, symbol) in stats.by_symbol" :key="symbol">
|
||
<div class="grade-card-header">
|
||
<span class="grade-card-title">{{ symbol }}</span>
|
||
</div>
|
||
<div class="grade-card-stats">
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">交易数</span>
|
||
<span>{{ data.count }}</span>
|
||
</div>
|
||
<div class="grade-stat-row">
|
||
<span class="grade-stat-label">胜率</span>
|
||
<span>{{ data.win_rate.toFixed(1) }}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 收益率图表 -->
|
||
<div v-show="activeTab === 'stats'">
|
||
<h3 style="color: var(--text-primary); font-weight: 300; margin: 30px 0 16px;">收益率分析</h3>
|
||
<!-- 图表 -->
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;">
|
||
<!-- 净值曲线图 -->
|
||
<div class="chart-container">
|
||
<h4 style="color: var(--text-primary); font-weight: 300; margin-bottom: 12px;">净值曲线</h4>
|
||
<div class="chart-wrapper">
|
||
<canvas id="balanceChart"></canvas>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 收益率柱状图 -->
|
||
<div class="chart-container">
|
||
<h4 style="color: var(--text-primary); font-weight: 300; margin-bottom: 12px;">每日收益率 (%)</h4>
|
||
<div class="chart-wrapper">
|
||
<canvas id="returnsChart"></canvas>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 表格 -->
|
||
<div v-if="loadingReturns" class="loading">加载中...</div>
|
||
<div v-else-if="dailyReturns.length === 0" class="empty-state">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
<path d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||
</svg>
|
||
<p>暂无收益率数据</p>
|
||
</div>
|
||
<div v-else class="table-wrapper">
|
||
<table class="daily-returns-table">
|
||
<thead>
|
||
<tr>
|
||
<th>日期</th>
|
||
<th>收益率</th>
|
||
<th>交易数</th>
|
||
<th>盈利</th>
|
||
<th>亏损</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="day in displayedDailyReturns" :key="day.date">
|
||
<td>{{ day.date }}</td>
|
||
<td :class="day.return_percent >= 0 ? 'positive' : 'negative'">
|
||
{{ day.return_percent >= 0 ? '+' : '' }}{{ day.return_percent }}%
|
||
</td>
|
||
<td>{{ day.trades_count }}</td>
|
||
<td class="positive">{{ day.winning_trades }}</td>
|
||
<td class="negative">{{ day.losing_trades }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分享卡片模态框 -->
|
||
<div v-if="showShareModal" class="share-modal-overlay" @click="closeShareModal">
|
||
<div class="share-modal" @click.stop>
|
||
<div class="share-modal-header">
|
||
<h3>分享持仓</h3>
|
||
<button class="close-btn" @click="closeShareModal">×</button>
|
||
</div>
|
||
<div class="share-card" ref="shareCard">
|
||
<div class="share-card-header">
|
||
<div class="share-logo">TRADUS</div>
|
||
<div class="share-time">{{ formatShareTime() }}</div>
|
||
</div>
|
||
<div class="share-card-body">
|
||
<div class="share-symbol">
|
||
<span class="symbol-name">{{ shareOrderData.symbol }}</span>
|
||
<span class="side-tag" :class="shareOrderData.side">
|
||
{{ shareOrderData.side === 'long' ? '做多' : '做空' }}
|
||
</span>
|
||
</div>
|
||
<div class="share-pnl" :class="shareOrderData.pnlPercent >= 0 ? 'positive' : 'negative'">
|
||
{{ shareOrderData.pnlPercent >= 0 ? '+' : '' }}{{ shareOrderData.pnlPercent.toFixed(2) }}%
|
||
</div>
|
||
<div class="share-prices">
|
||
<div class="price-item">
|
||
<span class="price-label">开仓价</span>
|
||
<span class="price-value">${{ shareOrderData.entryPrice?.toLocaleString() }}</span>
|
||
</div>
|
||
<div class="price-item">
|
||
<span class="price-label">现价</span>
|
||
<span class="price-value">${{ shareOrderData.currentPrice?.toLocaleString() }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="share-footer">
|
||
<div class="share-grade">
|
||
<span class="grade-badge" :class="shareOrderData.grade">{{ shareOrderData.grade }}</span>
|
||
<span class="grade-text">信号等级</span>
|
||
</div>
|
||
<div class="share-slogan">AI Agent Trading System</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="share-modal-actions">
|
||
<button class="share-btn download" @click="downloadShareImage">
|
||
📥 下载图片
|
||
</button>
|
||
<button class="share-btn copy" @click="copyShareImage">
|
||
📋 复制图片
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||
<script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
|
||
<script>
|
||
const { createApp } = Vue;
|
||
|
||
createApp({
|
||
data() {
|
||
return {
|
||
activeTab: 'active',
|
||
loading: true, // 只在首次加载时为 true
|
||
activeOrders: [],
|
||
historyOrders: [],
|
||
stats: {
|
||
total_trades: 0,
|
||
winning_trades: 0,
|
||
losing_trades: 0,
|
||
win_rate: 0,
|
||
total_pnl: 0,
|
||
average_win: 0,
|
||
average_loss: 0,
|
||
profit_factor: 0,
|
||
by_grade: {},
|
||
by_symbol: {}
|
||
},
|
||
account: {
|
||
initial_balance: 10000,
|
||
current_balance: 10000,
|
||
used_margin: 0,
|
||
available_margin: 10000,
|
||
leverage: 10,
|
||
margin_per_order: 1000,
|
||
active_orders: 0,
|
||
max_orders: 10,
|
||
available_orders: 10,
|
||
total_position_value: 0,
|
||
margin_ratio: 0,
|
||
realized_pnl: 0
|
||
},
|
||
monitorRunning: false,
|
||
latestPrices: {},
|
||
refreshInterval: null,
|
||
isFirstLoad: true,
|
||
sendingReport: false,
|
||
showAdminMenu: false,
|
||
adminPassword: '223388',
|
||
showShareModal: false,
|
||
shareOrderData: {
|
||
symbol: '',
|
||
side: 'long',
|
||
entryPrice: 0,
|
||
currentPrice: 0,
|
||
pnlPercent: 0,
|
||
pnlAmount: 0,
|
||
grade: 'B'
|
||
},
|
||
dailyReturns: [],
|
||
loadingReturns: false,
|
||
balanceChart: null,
|
||
returnsChart: null,
|
||
_renderingCharts: false // 防止重复渲染标志
|
||
};
|
||
},
|
||
mounted() {
|
||
this.refreshData();
|
||
// 每3秒自动刷新(静默刷新,不显示 loading)
|
||
this.refreshInterval = setInterval(() => {
|
||
this.silentRefresh();
|
||
}, 3000);
|
||
// 点击外部关闭菜单
|
||
document.addEventListener('click', this.closeAdminMenu);
|
||
},
|
||
beforeUnmount() {
|
||
if (this.refreshInterval) {
|
||
clearInterval(this.refreshInterval);
|
||
}
|
||
document.removeEventListener('click', this.closeAdminMenu);
|
||
},
|
||
methods: {
|
||
// 切换管理菜单
|
||
toggleAdminMenu(event) {
|
||
event.stopPropagation();
|
||
this.showAdminMenu = !this.showAdminMenu;
|
||
},
|
||
|
||
// 关闭管理菜单
|
||
closeAdminMenu(event) {
|
||
if (!event.target.closest('.admin-dropdown')) {
|
||
this.showAdminMenu = false;
|
||
}
|
||
},
|
||
|
||
// 验证管理密码
|
||
verifyAdminPassword() {
|
||
const password = prompt('请输入管理密码:');
|
||
return password === this.adminPassword;
|
||
},
|
||
|
||
// 管理操作:发送报告
|
||
async adminSendReport() {
|
||
this.showAdminMenu = false;
|
||
if (!this.verifyAdminPassword()) {
|
||
alert('密码错误');
|
||
return;
|
||
}
|
||
await this.sendReport();
|
||
},
|
||
|
||
// 管理操作:重置数据
|
||
async adminResetData() {
|
||
this.showAdminMenu = false;
|
||
if (!this.verifyAdminPassword()) {
|
||
alert('密码错误');
|
||
return;
|
||
}
|
||
await this.resetData();
|
||
},
|
||
|
||
// 分享订单
|
||
shareOrder(order) {
|
||
const currentPrice = this.getCurrentPrice(order.symbol);
|
||
const pnl = this.getUnrealizedPnl(order);
|
||
|
||
this.shareOrderData = {
|
||
symbol: order.symbol,
|
||
side: order.side,
|
||
entryPrice: order.entry_price,
|
||
currentPrice: currentPrice,
|
||
pnlPercent: pnl.percent,
|
||
pnlAmount: pnl.pnl,
|
||
grade: order.signal_grade
|
||
};
|
||
this.showShareModal = true;
|
||
},
|
||
|
||
// 关闭分享模态框
|
||
closeShareModal() {
|
||
this.showShareModal = false;
|
||
},
|
||
|
||
// 格式化分享时间
|
||
formatShareTime() {
|
||
return new Date().toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
},
|
||
|
||
// 下载分享图片
|
||
async downloadShareImage() {
|
||
const card = this.$refs.shareCard;
|
||
if (!card) return;
|
||
|
||
try {
|
||
const canvas = await html2canvas(card, {
|
||
backgroundColor: null,
|
||
scale: 2,
|
||
useCORS: true
|
||
});
|
||
|
||
const link = document.createElement('a');
|
||
link.download = `tradus-${this.shareOrderData.symbol}-${Date.now()}.png`;
|
||
link.href = canvas.toDataURL('image/png');
|
||
link.click();
|
||
} catch (e) {
|
||
console.error('生成图片失败:', e);
|
||
alert('生成图片失败,请重试');
|
||
}
|
||
},
|
||
|
||
// 复制分享图片
|
||
async copyShareImage() {
|
||
const card = this.$refs.shareCard;
|
||
if (!card) return;
|
||
|
||
try {
|
||
const canvas = await html2canvas(card, {
|
||
backgroundColor: null,
|
||
scale: 2,
|
||
useCORS: true
|
||
});
|
||
|
||
canvas.toBlob(async (blob) => {
|
||
try {
|
||
await navigator.clipboard.write([
|
||
new ClipboardItem({ 'image/png': blob })
|
||
]);
|
||
alert('图片已复制到剪贴板');
|
||
} catch (e) {
|
||
console.error('复制失败:', e);
|
||
alert('复制失败,请使用下载功能');
|
||
}
|
||
}, 'image/png');
|
||
} catch (e) {
|
||
console.error('生成图片失败:', e);
|
||
alert('生成图片失败,请重试');
|
||
}
|
||
},
|
||
|
||
// 静默刷新(不显示 loading)
|
||
async silentRefresh() {
|
||
await this.refreshData();
|
||
},
|
||
|
||
async refreshData() {
|
||
try {
|
||
await Promise.all([
|
||
this.fetchActiveOrders(),
|
||
this.fetchHistoryOrders(),
|
||
this.fetchStatistics(),
|
||
this.fetchAccountStatus(),
|
||
this.fetchMonitorStatus()
|
||
]);
|
||
|
||
// 首次加载时也获取每日收益率数据
|
||
if (this.isFirstLoad && this.dailyReturns.length === 0) {
|
||
await this.fetchDailyReturns();
|
||
}
|
||
} catch (e) {
|
||
console.error('刷新数据失败:', e);
|
||
} finally {
|
||
this.loading = false;
|
||
this.isFirstLoad = false;
|
||
}
|
||
},
|
||
|
||
async resetData() {
|
||
if (!confirm('确定要重置所有交易数据吗?\n\n此操作将删除所有订单记录,不可恢复!')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/paper-trading/reset', {
|
||
method: 'POST'
|
||
});
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
alert('数据已重置');
|
||
this.refreshData();
|
||
} else {
|
||
alert('重置失败: ' + data.detail);
|
||
}
|
||
} catch (e) {
|
||
alert('重置失败: ' + e.message);
|
||
}
|
||
},
|
||
|
||
async sendReport() {
|
||
this.sendingReport = true;
|
||
try {
|
||
const response = await fetch('/api/paper-trading/report?hours=4&send_telegram=true', {
|
||
method: 'POST'
|
||
});
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
if (data.telegram_sent) {
|
||
alert('报告已发送到 Telegram Channel');
|
||
} else {
|
||
alert('报告生成成功,但 Telegram 发送失败(可能未配置)');
|
||
}
|
||
} else {
|
||
alert('发送失败: ' + (data.detail || '未知错误'));
|
||
}
|
||
} catch (e) {
|
||
alert('发送失败: ' + e.message);
|
||
} finally {
|
||
this.sendingReport = false;
|
||
}
|
||
},
|
||
|
||
async fetchActiveOrders() {
|
||
const response = await fetch('/api/paper-trading/orders/active');
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
this.activeOrders = data.orders;
|
||
}
|
||
},
|
||
|
||
async fetchHistoryOrders() {
|
||
const response = await fetch('/api/paper-trading/orders?status=closed&limit=50');
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
this.historyOrders = data.orders;
|
||
}
|
||
},
|
||
|
||
async fetchStatistics() {
|
||
const response = await fetch('/api/paper-trading/statistics');
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
this.stats = data.statistics;
|
||
}
|
||
},
|
||
|
||
async fetchAccountStatus() {
|
||
const response = await fetch('/api/paper-trading/account');
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
this.account = data.account;
|
||
}
|
||
},
|
||
|
||
async fetchMonitorStatus() {
|
||
const response = await fetch('/api/paper-trading/monitor/status');
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
this.monitorRunning = data.running;
|
||
this.latestPrices = data.latest_prices || {};
|
||
}
|
||
},
|
||
|
||
async closeOrder(order) {
|
||
const price = this.latestPrices[order.symbol];
|
||
if (!price) {
|
||
alert('无法获取当前价格');
|
||
return;
|
||
}
|
||
|
||
if (!confirm(`确定要以 $${price.toLocaleString()} 平仓吗?`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/paper-trading/orders/${order.order_id}/close`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ exit_price: price })
|
||
});
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
// 区分平仓和取消挂单
|
||
if (data.result.pnl_percent !== undefined) {
|
||
alert(`平仓成功!盈亏: ${data.result.pnl_percent.toFixed(2)}%`);
|
||
} else {
|
||
alert('挂单已取消');
|
||
}
|
||
this.refreshData();
|
||
} else {
|
||
alert('平仓失败: ' + (data.detail || '未知错误'));
|
||
}
|
||
} catch (e) {
|
||
alert('平仓失败: ' + e.message);
|
||
}
|
||
},
|
||
|
||
formatTime(timeStr) {
|
||
if (!timeStr) return '-';
|
||
const date = new Date(timeStr);
|
||
return date.toLocaleString('zh-CN', {
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
},
|
||
|
||
formatStatus(status) {
|
||
const map = {
|
||
'open': '持仓中',
|
||
'closed_tp': '止盈',
|
||
'closed_sl': '止损',
|
||
'closed_be': '保本止损',
|
||
'closed_manual': '手动平仓'
|
||
};
|
||
return map[status] || status;
|
||
},
|
||
|
||
// 获取当前价格
|
||
getCurrentPrice(symbol) {
|
||
return this.latestPrices[symbol] || null;
|
||
},
|
||
|
||
// 计算未实现盈亏
|
||
getUnrealizedPnl(order) {
|
||
const currentPrice = this.getCurrentPrice(order.symbol);
|
||
if (!currentPrice || !order.entry_price) {
|
||
return { pnl: 0, percent: 0 };
|
||
}
|
||
|
||
let pnlPercent;
|
||
if (order.side === 'long') {
|
||
pnlPercent = ((currentPrice - order.entry_price) / order.entry_price) * 100;
|
||
} else {
|
||
pnlPercent = ((order.entry_price - currentPrice) / order.entry_price) * 100;
|
||
}
|
||
|
||
const pnlAmount = (order.quantity || 0) * pnlPercent / 100;
|
||
|
||
return {
|
||
pnl: pnlAmount,
|
||
percent: pnlPercent
|
||
};
|
||
},
|
||
|
||
// 获取价格变化样式类
|
||
getPriceChangeClass(order) {
|
||
const currentPrice = this.getCurrentPrice(order.symbol);
|
||
if (!currentPrice || !order.entry_price) return '';
|
||
|
||
if (order.side === 'long') {
|
||
return currentPrice >= order.entry_price ? 'price-up' : 'price-down';
|
||
} else {
|
||
return currentPrice <= order.entry_price ? 'price-up' : 'price-down';
|
||
}
|
||
},
|
||
|
||
// 检查是否接近止损(距离止损 < 1%)
|
||
isNearStopLoss(order) {
|
||
const currentPrice = this.getCurrentPrice(order.symbol);
|
||
if (!currentPrice || !order.stop_loss) return false;
|
||
|
||
const distance = Math.abs(currentPrice - order.stop_loss) / currentPrice;
|
||
return distance < 0.01;
|
||
},
|
||
|
||
// 检查是否接近止盈(距离止盈 < 1%)
|
||
isNearTakeProfit(order) {
|
||
const currentPrice = this.getCurrentPrice(order.symbol);
|
||
if (!currentPrice || !order.take_profit) return false;
|
||
|
||
const distance = Math.abs(currentPrice - order.take_profit) / currentPrice;
|
||
return distance < 0.01;
|
||
},
|
||
|
||
// 获取每日收益率数据
|
||
async fetchDailyReturns() {
|
||
this.loadingReturns = true;
|
||
try {
|
||
const response = await fetch('/api/paper-trading/daily-returns?days=30');
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
this.dailyReturns = data.data;
|
||
// 等待 DOM 更新后渲染图表,添加额外延迟确保 canvas 完全渲染
|
||
this.$nextTick(() => {
|
||
setTimeout(() => {
|
||
this.renderCharts();
|
||
}, 100);
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.error('获取每日收益率失败:', e);
|
||
} finally {
|
||
this.loadingReturns = false;
|
||
}
|
||
},
|
||
|
||
// 渲染图表
|
||
renderCharts() {
|
||
// 防止重复渲染
|
||
if (this._renderingCharts) {
|
||
console.log('Charts already rendering, skipping...');
|
||
return;
|
||
}
|
||
this._renderingCharts = true;
|
||
|
||
try {
|
||
this.renderBalanceChart();
|
||
this.renderReturnsChart();
|
||
} finally {
|
||
// 延迟重置标志,确保渲染完成
|
||
setTimeout(() => {
|
||
this._renderingCharts = false;
|
||
}, 200);
|
||
}
|
||
},
|
||
|
||
// 渲染净值曲线图
|
||
renderBalanceChart() {
|
||
const canvas = document.getElementById('balanceChart');
|
||
if (!canvas) {
|
||
console.warn('balanceChart canvas not found');
|
||
return;
|
||
}
|
||
|
||
// 检查 canvas 是否可见(offsetParent 不为 null 表示元素可见)
|
||
if (!canvas.offsetParent) {
|
||
console.warn('balanceChart canvas is not visible');
|
||
return;
|
||
}
|
||
|
||
// 销毁已存在的图表
|
||
if (this.balanceChart) {
|
||
this.balanceChart.destroy();
|
||
this.balanceChart = null;
|
||
}
|
||
|
||
const ctx = canvas.getContext('2d');
|
||
if (!ctx) {
|
||
console.warn('Cannot get 2d context for balanceChart');
|
||
return;
|
||
}
|
||
|
||
// 确保有数据
|
||
if (!this.dailyReturns || this.dailyReturns.length === 0) {
|
||
console.warn('No daily returns data');
|
||
return;
|
||
}
|
||
|
||
const labels = this.dailyReturns.map(d => d.date);
|
||
const balanceData = this.dailyReturns.map(d => d.balance);
|
||
|
||
console.log('Rendering balance chart:', { labels, balanceData });
|
||
|
||
try {
|
||
this.balanceChart = new Chart(ctx, {
|
||
type: 'line',
|
||
data: {
|
||
labels: labels,
|
||
datasets: [{
|
||
label: '账户净值',
|
||
data: balanceData,
|
||
borderColor: '#00ff41',
|
||
backgroundColor: 'rgba(0, 255, 65, 0.1)',
|
||
borderWidth: 2,
|
||
fill: true,
|
||
tension: 0.4,
|
||
pointRadius: 2,
|
||
pointHoverRadius: 5
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: {
|
||
display: false
|
||
},
|
||
tooltip: {
|
||
mode: 'index',
|
||
intersect: false,
|
||
callbacks: {
|
||
label: function(context) {
|
||
return '净值: $' + context.parsed.y.toFixed(2);
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
x: {
|
||
display: true,
|
||
grid: {
|
||
color: 'rgba(255, 255, 255, 0.1)'
|
||
},
|
||
ticks: {
|
||
color: 'rgba(255, 255, 255, 0.6)',
|
||
maxRotation: 45,
|
||
minRotation: 45
|
||
}
|
||
},
|
||
y: {
|
||
display: true,
|
||
grid: {
|
||
color: 'rgba(255, 255, 255, 0.1)'
|
||
},
|
||
ticks: {
|
||
color: 'rgba(255, 255, 255, 0.6)',
|
||
callback: function(value) {
|
||
return '$' + value.toFixed(0);
|
||
}
|
||
}
|
||
}
|
||
},
|
||
interaction: {
|
||
mode: 'nearest',
|
||
axis: 'x',
|
||
intersect: false
|
||
}
|
||
}
|
||
});
|
||
console.log('Balance chart rendered successfully');
|
||
} catch (e) {
|
||
console.error('Error rendering balance chart:', e);
|
||
}
|
||
},
|
||
|
||
// 渲染收益率柱状图
|
||
renderReturnsChart() {
|
||
const canvas = document.getElementById('returnsChart');
|
||
if (!canvas) {
|
||
console.warn('returnsChart canvas not found');
|
||
return;
|
||
}
|
||
|
||
// 检查 canvas 是否可见
|
||
if (!canvas.offsetParent) {
|
||
console.warn('returnsChart canvas is not visible');
|
||
return;
|
||
}
|
||
|
||
// 销毁已存在的图表
|
||
if (this.returnsChart) {
|
||
this.returnsChart.destroy();
|
||
this.returnsChart = null;
|
||
}
|
||
|
||
const ctx = canvas.getContext('2d');
|
||
if (!ctx) {
|
||
console.warn('Cannot get 2d context for returnsChart');
|
||
return;
|
||
}
|
||
|
||
// 确保有数据
|
||
if (!this.dailyReturns || this.dailyReturns.length === 0) {
|
||
console.warn('No daily returns data');
|
||
return;
|
||
}
|
||
|
||
const labels = this.dailyReturns.map(d => d.date);
|
||
const returnsData = this.dailyReturns.map(d => d.return_percent);
|
||
const colors = returnsData.map(v => v >= 0 ? '#00ff41' : '#ff4444');
|
||
|
||
console.log('Rendering returns chart:', { labels, returnsData });
|
||
|
||
try {
|
||
this.returnsChart = new Chart(ctx, {
|
||
type: 'bar',
|
||
data: {
|
||
labels: labels,
|
||
datasets: [{
|
||
label: '日收益率',
|
||
data: returnsData,
|
||
backgroundColor: colors,
|
||
borderColor: colors,
|
||
borderWidth: 1
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: {
|
||
display: false
|
||
},
|
||
tooltip: {
|
||
callbacks: {
|
||
label: function(context) {
|
||
const value = context.parsed.y;
|
||
return '收益率: ' + (value >= 0 ? '+' : '') + value.toFixed(2) + '%';
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
x: {
|
||
display: true,
|
||
grid: {
|
||
display: false
|
||
},
|
||
ticks: {
|
||
color: 'rgba(255, 255, 255, 0.6)',
|
||
maxRotation: 45,
|
||
minRotation: 45
|
||
}
|
||
},
|
||
y: {
|
||
display: true,
|
||
grid: {
|
||
color: 'rgba(255, 255, 255, 0.1)'
|
||
},
|
||
ticks: {
|
||
color: 'rgba(255, 255, 255, 0.6)',
|
||
callback: function(value) {
|
||
return value.toFixed(1) + '%';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
console.log('Returns chart rendered successfully');
|
||
} catch (e) {
|
||
console.error('Error rendering returns chart:', e);
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
// 只计算已开仓的订单(不包括挂单)
|
||
openOrders() {
|
||
return this.activeOrders.filter(order => order.status === 'open');
|
||
},
|
||
|
||
// 持仓数量(只计算已开仓)
|
||
openOrdersCount() {
|
||
return this.openOrders.length;
|
||
},
|
||
|
||
// 总仓位(只计算已开仓)
|
||
totalPosition() {
|
||
return this.openOrders.reduce((sum, order) => sum + (order.quantity || 0), 0);
|
||
},
|
||
|
||
// 总浮动盈亏(只计算已开仓)
|
||
totalUnrealizedPnl() {
|
||
return this.openOrders.reduce((sum, order) => {
|
||
return sum + this.getUnrealizedPnl(order).pnl;
|
||
}, 0);
|
||
},
|
||
|
||
// 总浮动盈亏百分比
|
||
totalUnrealizedPnlPercent() {
|
||
if (this.totalPosition === 0) return 0;
|
||
return (this.totalUnrealizedPnl / this.totalPosition) * 100;
|
||
},
|
||
|
||
// 累计收益率
|
||
totalReturn() {
|
||
if (this.dailyReturns.length === 0) return 0;
|
||
const firstDay = this.dailyReturns[0];
|
||
const lastDay = this.dailyReturns[this.dailyReturns.length - 1];
|
||
if (firstDay.balance === 0) return 0;
|
||
return ((lastDay.balance - firstDay.balance) / firstDay.balance) * 100;
|
||
},
|
||
|
||
// 累计收益额
|
||
totalReturnAmount() {
|
||
if (this.dailyReturns.length === 0) return 0;
|
||
const lastDay = this.dailyReturns[this.dailyReturns.length - 1];
|
||
const firstDay = this.dailyReturns[0];
|
||
return lastDay.balance - firstDay.balance;
|
||
},
|
||
|
||
// 盈利天数
|
||
profitableDays() {
|
||
return this.dailyReturns.filter(d => d.return_percent > 0).length;
|
||
},
|
||
|
||
// 亏损天数
|
||
losingDays() {
|
||
return this.dailyReturns.filter(d => d.return_percent < 0).length;
|
||
},
|
||
|
||
// 日胜率
|
||
dailyWinRate() {
|
||
const totalDays = this.dailyReturns.filter(d => d.trades_count > 0).length;
|
||
if (totalDays === 0) return 0;
|
||
return (this.profitableDays / totalDays) * 100;
|
||
},
|
||
|
||
// 平均日收益
|
||
avgDailyReturn() {
|
||
const tradingDays = this.dailyReturns.filter(d => d.trades_count > 0).length;
|
||
if (tradingDays === 0) return 0;
|
||
const totalReturn = this.dailyReturns.reduce((sum, d) => sum + d.return_percent, 0);
|
||
return totalReturn / tradingDays;
|
||
},
|
||
|
||
// 显示的每日收益率(倒序,前15条)
|
||
displayedDailyReturns() {
|
||
return [...this.dailyReturns].reverse().slice(0, 15);
|
||
}
|
||
},
|
||
watch: {
|
||
// 监听标签页切换,加载收益率数据并渲染图表
|
||
activeTab(newTab) {
|
||
if (newTab === 'returns') {
|
||
if (this.dailyReturns.length === 0) {
|
||
this.fetchDailyReturns();
|
||
} else {
|
||
// 数据已存在,需要重新渲染图表,添加延迟确保 DOM 显示
|
||
this.$nextTick(() => {
|
||
setTimeout(() => {
|
||
this.renderCharts();
|
||
}, 100);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}).mount('#app');
|
||
</script>
|
||
</body>
|
||
</html>
|