415 lines
23 KiB
HTML
415 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>BTC 预测交易看板</title>
|
|
<!-- Bootstrap 5 CSS -->
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<!-- Bootstrap Icons -->
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
|
|
<!-- Custom CSS -->
|
|
<link href="/static/style.css" rel="stylesheet">
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<!-- 导航栏 -->
|
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
|
<div class="container">
|
|
<a class="navbar-brand" href="#">
|
|
<i class="bi bi-currency-bitcoin"></i> BTC 预测交易看板
|
|
</a>
|
|
<span class="navbar-text text-light">
|
|
<i class="bi bi-clock"></i> {{ serverTime }}
|
|
</span>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- 主容器 -->
|
|
<div class="container mt-4">
|
|
<!-- Tab 导航 -->
|
|
<ul class="nav nav-tabs mb-4" role="tablist">
|
|
<li class="nav-item">
|
|
<button class="nav-link" :class="{ active: activeTab === 'trades' }"
|
|
@click="activeTab = 'trades'">
|
|
<i class="bi bi-graph-up"></i> 交易详情
|
|
</button>
|
|
</li>
|
|
<li class="nav-item">
|
|
<button class="nav-link" :class="{ active: activeTab === 'accuracy' }"
|
|
@click="activeTab = 'accuracy'">
|
|
<i class="bi bi-bullseye"></i> 模型准确率
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- 交易详情 Tab -->
|
|
<div v-show="activeTab === 'trades'">
|
|
<!-- 当前交易卡片 -->
|
|
<div class="card mb-4">
|
|
<div class="card-header bg-primary text-white">
|
|
<i class="bi bi-lightning-charge"></i> 当前小时交易
|
|
</div>
|
|
<div class="card-body">
|
|
<div v-if="currentTrade" class="row">
|
|
<div class="col-md-3">
|
|
<div class="stat-card">
|
|
<div class="stat-label">市场</div>
|
|
<div class="stat-value small">{{ currentTrade.market_title || '-' }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="stat-card">
|
|
<div class="stat-label">预测方向</div>
|
|
<div class="stat-value" :class="getPredictionClass(currentTrade.prediction)">
|
|
{{ currentTrade.prediction || '-' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="stat-card">
|
|
<div class="stat-label">概率</div>
|
|
<div class="stat-value">{{ formatPercent(currentTrade.probability) }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="stat-card">
|
|
<div class="stat-label">价格</div>
|
|
<div class="stat-value">${{ currentTrade.price?.toFixed(3) || '-' }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="stat-card">
|
|
<div class="stat-label">状态</div>
|
|
<div class="stat-value" :class="getStatusClass(currentTrade.status)">
|
|
{{ getStatusText(currentTrade.status) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="text-center text-muted py-4">
|
|
<i class="bi bi-inbox"></i> 暂无交易记录
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 历史交易 -->
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<span><i class="bi bi-clock-history"></i> 历史交易记录</span>
|
|
<div class="d-flex gap-2">
|
|
<input type="month" class="form-control form-control-sm"
|
|
v-model="tradeFilter.month" @change="loadTradeHistory(1)"
|
|
style="width: 150px;">
|
|
<input type="date" class="form-control form-control-sm"
|
|
v-model="tradeFilter.day" @change="loadTradeHistory(1)"
|
|
style="width: 150px;">
|
|
<button class="btn btn-sm btn-outline-secondary" @click="clearTradeFilter">
|
|
<i class="bi bi-x-circle"></i> 清除
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover table-striped mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>时间 (北京)</th>
|
|
<th>市场</th>
|
|
<th>预测</th>
|
|
<th>概率</th>
|
|
<th>价格</th>
|
|
<th>状态</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="trade in tradeHistory.data" :key="trade.id">
|
|
<td>{{ formatTime(trade.timestamp, trade.timestamp_cst) }}</td>
|
|
<td class="text-truncate" style="max-width: 200px;">
|
|
{{ trade.market_title }}
|
|
</td>
|
|
<td :class="getPredictionClass(trade.prediction)">
|
|
<i :class="trade.prediction === 'UP' ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
|
{{ trade.prediction }}
|
|
</td>
|
|
<td>{{ formatPercent(trade.probability) }}</td>
|
|
<td>${{ trade.price?.toFixed(3) }}</td>
|
|
<td :class="getStatusClass(trade.status)">
|
|
{{ getStatusText(trade.status) }}
|
|
</td>
|
|
</tr>
|
|
<tr v-if="!tradeHistory.data?.length">
|
|
<td colspan="6" class="text-center text-muted py-4">
|
|
暂无数据
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<!-- 分页 -->
|
|
<div class="card-footer d-flex justify-content-between align-items-center">
|
|
<span class="text-muted">共 {{ tradeHistory.total }} 条</span>
|
|
<nav>
|
|
<ul class="pagination pagination-sm mb-0">
|
|
<li class="page-item" :class="{ disabled: tradeHistory.page <= 1 }">
|
|
<a class="page-link" href="#" @click.prevent="loadTradeHistory(tradeHistory.page - 1)">
|
|
上一页
|
|
</a>
|
|
</li>
|
|
<li class="page-item disabled">
|
|
<span class="page-link">{{ tradeHistory.page }} / {{ tradeHistory.total_pages }}</span>
|
|
</li>
|
|
<li class="page-item" :class="{ disabled: tradeHistory.page >= tradeHistory.total_pages }">
|
|
<a class="page-link" href="#" @click.prevent="loadTradeHistory(tradeHistory.page + 1)">
|
|
下一页
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 模型准确率 Tab -->
|
|
<div v-show="activeTab === 'accuracy'">
|
|
<!-- 月份筛选 -->
|
|
<div class="mb-4">
|
|
<div class="d-flex gap-2 align-items-center">
|
|
<label class="form-label mb-0">选择月份:</label>
|
|
<input type="month" class="form-control" v-model="accuracyFilter.month"
|
|
@change="loadAccuracyData" style="width: 180px;">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 当月汇总卡片 -->
|
|
<div class="row mb-4">
|
|
<!-- DLinear 卡片 -->
|
|
<div class="col-md-6">
|
|
<div class="card h-100 border-primary">
|
|
<div class="card-header bg-primary text-white">
|
|
<i class="bi bi-graph-up-arrow"></i> DLinear 模型
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row text-center" v-if="monthlyStats.DLinear">
|
|
<div class="col-3">
|
|
<div class="stat-number">{{ monthlyStats.DLinear.total }}</div>
|
|
<div class="stat-label">预测次数</div>
|
|
</div>
|
|
<div class="col-3">
|
|
<div class="stat-number text-success">{{ monthlyStats.DLinear.correct }}</div>
|
|
<div class="stat-label">正确</div>
|
|
</div>
|
|
<div class="col-3">
|
|
<div class="stat-number text-danger">{{ monthlyStats.DLinear.incorrect }}</div>
|
|
<div class="stat-label">错误</div>
|
|
</div>
|
|
<div class="col-3">
|
|
<div class="stat-number text-primary">{{ monthlyStats.DLinear.accuracy }}%</div>
|
|
<div class="stat-label">准确率</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="text-center text-muted py-3">
|
|
暂无数据
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TTM-R2 卡片 -->
|
|
<div class="col-md-6">
|
|
<div class="card h-100 border-success">
|
|
<div class="card-header bg-success text-white">
|
|
<i class="bi bi-cpu"></i> TTM-R2 模型
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row text-center" v-if="monthlyStats['TTM-R2']">
|
|
<div class="col-3">
|
|
<div class="stat-number">{{ monthlyStats['TTM-R2'].total }}</div>
|
|
<div class="stat-label">预测次数</div>
|
|
</div>
|
|
<div class="col-3">
|
|
<div class="stat-number text-success">{{ monthlyStats['TTM-R2'].correct }}</div>
|
|
<div class="stat-label">正确</div>
|
|
</div>
|
|
<div class="col-3">
|
|
<div class="stat-number text-danger">{{ monthlyStats['TTM-R2'].incorrect }}</div>
|
|
<div class="stat-label">错误</div>
|
|
</div>
|
|
<div class="col-3">
|
|
<div class="stat-number text-primary">{{ monthlyStats['TTM-R2'].accuracy }}%</div>
|
|
<div class="stat-label">准确率</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="text-center text-muted py-3">
|
|
暂无数据
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 每日准确率表格 -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<i class="bi bi-calendar3"></i> 每日准确率
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>日期</th>
|
|
<th>DLinear 预测</th>
|
|
<th>DLinear 准确率</th>
|
|
<th>TTM-R2 预测</th>
|
|
<th>TTM-R2 准确率</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="day in dailyStats.data" :key="day.date">
|
|
<td>{{ day.date }}</td>
|
|
<td>
|
|
<span v-if="day.DLinear">
|
|
{{ day.DLinear.correct }}/{{ day.DLinear.total }}
|
|
</span>
|
|
<span v-else class="text-muted">-</span>
|
|
</td>
|
|
<td>
|
|
<span v-if="day.DLinear" :class="getAccuracyClass(day.DLinear.accuracy)">
|
|
{{ day.DLinear.accuracy }}%
|
|
</span>
|
|
<span v-else class="text-muted">-</span>
|
|
</td>
|
|
<td>
|
|
<span v-if="day['TTM-R2']">
|
|
{{ day['TTM-R2'].correct }}/{{ day['TTM-R2'].total }}
|
|
</span>
|
|
<span v-else class="text-muted">-</span>
|
|
</td>
|
|
<td>
|
|
<span v-if="day['TTM-R2']" :class="getAccuracyClass(day['TTM-R2'].accuracy)">
|
|
{{ day['TTM-R2'].accuracy }}%
|
|
</span>
|
|
<span v-else class="text-muted">-</span>
|
|
</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-outline-primary"
|
|
@click="showHourlyDetails(day.date)">
|
|
<i class="bi bi-eye"></i> 查看
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="!dailyStats.data?.length">
|
|
<td colspan="6" class="text-center text-muted py-4">
|
|
暂无数据
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<!-- 分页 -->
|
|
<div class="card-footer d-flex justify-content-between align-items-center">
|
|
<span class="text-muted">共 {{ dailyStats.total }} 天</span>
|
|
<nav>
|
|
<ul class="pagination pagination-sm mb-0">
|
|
<li class="page-item" :class="{ disabled: dailyStats.page <= 1 }">
|
|
<a class="page-link" href="#" @click.prevent="loadDailyStats(dailyStats.page - 1)">
|
|
上一页
|
|
</a>
|
|
</li>
|
|
<li class="page-item disabled">
|
|
<span class="page-link">{{ dailyStats.page }} / {{ dailyStats.total_pages }}</span>
|
|
</li>
|
|
<li class="page-item" :class="{ disabled: dailyStats.page >= dailyStats.total_pages }">
|
|
<a class="page-link" href="#" @click.prevent="loadDailyStats(dailyStats.page + 1)">
|
|
下一页
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 每小时详情弹窗 -->
|
|
<div class="modal fade" id="hourlyModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="bi bi-clock"></i> {{ hourlyModal.date }} 每小时预测详情
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<table class="table table-sm table-hover mb-0">
|
|
<thead class="table-light sticky-top">
|
|
<tr>
|
|
<th>时间 (北京)</th>
|
|
<th>实际涨跌</th>
|
|
<th>DLinear</th>
|
|
<th>TTM-R2</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="hour in hourlyModal.data" :key="hour.hour">
|
|
<td>{{ hour.timestamp_cst || (hour.hour + ':00') }}</td>
|
|
<td :class="hour.actual_result === 1 ? 'text-success' : 'text-danger'">
|
|
<i :class="hour.actual_result === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
|
{{ hour.actual_result === 1 ? 'UP' : 'DOWN' }}
|
|
</td>
|
|
<td>
|
|
<span v-if="hour.models?.DLinear"
|
|
:class="hour.models.DLinear.is_correct ? 'text-success' : 'text-danger'">
|
|
<i :class="hour.models.DLinear.prediction === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
|
{{ hour.models.DLinear.prediction === 1 ? 'UP' : 'DOWN' }}
|
|
<i :class="hour.models.DLinear.is_correct ? 'bi-check-circle' : 'bi-x-circle'"></i>
|
|
</span>
|
|
<span v-else class="text-muted">-</span>
|
|
</td>
|
|
<td>
|
|
<span v-if="hour.models?.['TTM-R2']"
|
|
:class="hour.models['TTM-R2'].is_correct ? 'text-success' : 'text-danger'">
|
|
<i :class="hour.models['TTM-R2'].prediction === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
|
{{ hour.models['TTM-R2'].prediction === 1 ? 'UP' : 'DOWN' }}
|
|
<i :class="hour.models['TTM-R2'].is_correct ? 'bi-check-circle' : 'bi-x-circle'"></i>
|
|
</span>
|
|
<span v-else class="text-muted">-</span>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="!hourlyModal.data?.length">
|
|
<td colspan="4" class="text-center text-muted py-4">
|
|
暂无数据
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading 遮罩 -->
|
|
<div v-if="loading" class="loading-overlay">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">加载中...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bootstrap 5 JS -->
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
|
<!-- Vue 3 -->
|
|
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.15/dist/vue.global.prod.js"></script>
|
|
<!-- App JS -->
|
|
<script src="/static/app.js"></script>
|
|
</body>
|
|
</html>
|