stock-ai-agent/frontend/paper-trading.html
2026-02-15 14:20:11 +08:00

1286 lines
45 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tradus Auto Trading | Based on AI Agent</title>
<link rel="stylesheet" href="/static/css/style.css">
<style>
/* 覆盖全局 #app 样式 */
#app {
height: auto;
display: block;
align-items: initial;
justify-content: initial;
padding: 0;
}
.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_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;
}
.trading-container {
min-width: auto;
max-width: 100%;
}
.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;
}
.stat-value {
font-size: 18px;
}
.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;
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
.trading-title {
font-size: 16px;
}
.stat-value {
font-size: 16px;
}
}
</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>
<!-- 实时价格 -->
<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>
<button class="action-btn danger" @click="closeOrder(order)">
{{ order.status === 'pending' ? '取消' : '平仓' }}
</button>
</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'">
<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 class="grade-stat-row">
<span class="grade-stat-label">总盈亏</span>
<span :class="data.total_pnl >= 0 ? 'positive' : 'negative'">
${{ data.total_pnl.toFixed(2) }}
</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 class="grade-stat-row">
<span class="grade-stat-label">总盈亏</span>
<span :class="data.total_pnl >= 0 ? 'positive' : 'negative'">
${{ data.total_pnl.toFixed(2) }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.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'
};
},
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();
},
// 静默刷新(不显示 loading
async silentRefresh() {
await this.refreshData();
},
async refreshData() {
try {
await Promise.all([
this.fetchActiveOrders(),
this.fetchHistoryOrders(),
this.fetchStatistics(),
this.fetchAccountStatus(),
this.fetchMonitorStatus()
]);
} 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) {
alert(`平仓成功!盈亏: ${data.result.pnl_percent.toFixed(2)}%`);
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_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;
}
},
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;
}
}
}).mount('#app');
</script>
</body>
</html>