- Updated DB schemas: added 'strategy' to trades, new 'hourly_model_predictions' wide table - Refactored backend: api/models.py, api/prediction_service.py to support wide format - Updated trader: auto_trade_polymarket.py now logs strategy and saves to new table - Updated frontend: index.html and app.js to display multi-model accuracy and trade strategy
469 lines
27 KiB
HTML
469 lines
27 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 small">{{ currentTrade.strategy || 'Ensemble' }}</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>
|
|
<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>
|
|
<span class="badge bg-secondary">{{ trade.strategy || 'Ensemble' }}</span>
|
|
</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="7" 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">
|
|
<!-- Ensemble 卡片 -->
|
|
<div class="col-md-3">
|
|
<div class="card h-100 border-primary shadow-sm">
|
|
<div class="card-header bg-primary text-white text-center">
|
|
<i class="bi bi-diagram-3"></i> 集成策略 (投票)
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="text-center" v-if="monthlyStats.Ensemble">
|
|
<h2 class="display-6 text-primary fw-bold">{{ monthlyStats.Ensemble.accuracy }}%</h2>
|
|
<div class="text-muted mb-2">准确率</div>
|
|
<div class="d-flex justify-content-between px-3 border-top pt-2">
|
|
<span class="text-success"><i class="bi bi-check-lg"></i> {{ monthlyStats.Ensemble.correct }}</span>
|
|
<span class="text-secondary">{{ monthlyStats.Ensemble.total }} 次</span>
|
|
<span class="text-danger"><i class="bi bi-x-lg"></i> {{ monthlyStats.Ensemble.incorrect }}</span>
|
|
</div>
|
|
</div>
|
|
<div v-else class="text-center text-muted py-3">暂无数据</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- DLinear 卡片 -->
|
|
<div class="col-md-3">
|
|
<div class="card h-100 border-secondary">
|
|
<div class="card-header bg-secondary text-white text-center">
|
|
<i class="bi bi-graph-up-arrow"></i> DLinear
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="text-center" v-if="monthlyStats.DLinear">
|
|
<h3 class="text-dark">{{ monthlyStats.DLinear.accuracy }}%</h3>
|
|
<div class="text-muted small">准确率</div>
|
|
<div class="mt-2 small text-muted">
|
|
{{ monthlyStats.DLinear.correct }}/{{ monthlyStats.DLinear.total }}
|
|
</div>
|
|
</div>
|
|
<div v-else class="text-center text-muted py-3">暂无数据</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- N-BEATS 卡片 -->
|
|
<div class="col-md-3">
|
|
<div class="card h-100 border-secondary">
|
|
<div class="card-header bg-secondary text-white text-center">
|
|
<i class="bi bi-soundwave"></i> N-BEATS
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="text-center" v-if="monthlyStats['N-BEATS']">
|
|
<h3 class="text-dark">{{ monthlyStats['N-BEATS'].accuracy }}%</h3>
|
|
<div class="text-muted small">准确率</div>
|
|
<div class="mt-2 small text-muted">
|
|
{{ monthlyStats['N-BEATS'].correct }}/{{ monthlyStats['N-BEATS'].total }}
|
|
</div>
|
|
</div>
|
|
<div v-else class="text-center text-muted py-3">暂无数据</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TTM-R2 卡片 -->
|
|
<div class="col-md-3">
|
|
<div class="card h-100 border-secondary">
|
|
<div class="card-header bg-secondary text-white text-center">
|
|
<i class="bi bi-cpu"></i> TTM-R2
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="text-center" v-if="monthlyStats['TTM-R2']">
|
|
<h3 class="text-dark">{{ monthlyStats['TTM-R2'].accuracy }}%</h3>
|
|
<div class="text-muted small">准确率</div>
|
|
<div class="mt-2 small text-muted">
|
|
{{ monthlyStats['TTM-R2'].correct }}/{{ monthlyStats['TTM-R2'].total }}
|
|
</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 text-center">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th class="text-start ps-4">日期</th>
|
|
<th>集成策略</th>
|
|
<th>DLinear</th>
|
|
<th>N-BEATS</th>
|
|
<th>TTM-R2</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="day in dailyStats.data" :key="day.date">
|
|
<td class="text-start ps-4">{{ day.date }}</td>
|
|
|
|
<!-- Ensemble -->
|
|
<td>
|
|
<span v-if="day.Ensemble" :class="getAccuracyClass(day.Ensemble.accuracy)">
|
|
{{ day.Ensemble.accuracy }}%
|
|
</span>
|
|
<span v-else class="text-muted">-</span>
|
|
</td>
|
|
|
|
<!-- DLinear -->
|
|
<td>
|
|
<span v-if="day.DLinear" :class="getAccuracyClass(day.DLinear.accuracy)">
|
|
{{ day.DLinear.accuracy }}%
|
|
</span>
|
|
<span v-else class="text-muted">-</span>
|
|
</td>
|
|
|
|
<!-- N-BEATS -->
|
|
<td>
|
|
<span v-if="day['N-BEATS']" :class="getAccuracyClass(day['N-BEATS'].accuracy)">
|
|
{{ day['N-BEATS'].accuracy }}%
|
|
</span>
|
|
<span v-else class="text-muted">-</span>
|
|
</td>
|
|
|
|
<!-- TTM-R2 -->
|
|
<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-xl 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 text-center align-middle">
|
|
<thead class="table-light sticky-top">
|
|
<tr>
|
|
<th>时间</th>
|
|
<th>开盘</th>
|
|
<th>收盘</th>
|
|
<th>实际</th>
|
|
<th class="table-primary">集成策略</th>
|
|
<th>DLinear</th>
|
|
<th>N-BEATS</th>
|
|
<th>TTM-R2</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="hour in hourlyModal.data" :key="hour.hour">
|
|
<td>{{ hour.timestamp_cst ? hour.timestamp_cst.split(' ')[1] : (hour.hour + ':00') }}</td>
|
|
<td>${{ hour.open_price?.toFixed(0) || '-' }}</td>
|
|
<td>${{ hour.close_price?.toFixed(0) || '-' }}</td>
|
|
|
|
<td :class="hour.actual_result === 1 ? 'text-success fw-bold' : (hour.actual_result === 0 ? 'text-danger fw-bold' : 'text-muted')">
|
|
<i v-if="hour.actual_result !== null" :class="hour.actual_result === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
|
{{ hour.actual_result === 1 ? 'UP' : (hour.actual_result === 0 ? 'DOWN' : '-') }}
|
|
</td>
|
|
|
|
<!-- Ensemble -->
|
|
<td class="table-primary bg-opacity-10">
|
|
<span v-if="hour.models?.Ensemble"
|
|
:class="hour.models.Ensemble.is_correct ? 'text-success' : 'text-danger'">
|
|
<i :class="hour.models.Ensemble.prediction === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
|
{{ hour.models.Ensemble.prediction === 1 ? 'UP' : 'DOWN' }}
|
|
<i v-if="hour.actual_result !== null" :class="hour.models.Ensemble.is_correct ? 'bi-check-circle-fill' : 'bi-x-circle-fill'"></i>
|
|
</span>
|
|
</td>
|
|
|
|
<!-- DLinear -->
|
|
<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>
|
|
<span class="small text-muted">({{ (hour.models.DLinear.probability * 100).toFixed(0) }}%)</span>
|
|
<i v-if="hour.actual_result !== null" :class="hour.models.DLinear.is_correct ? 'bi-check' : 'bi-x'"></i>
|
|
</span>
|
|
</td>
|
|
|
|
<!-- N-BEATS -->
|
|
<td>
|
|
<span v-if="hour.models?.['N-BEATS']"
|
|
:class="hour.models['N-BEATS'].is_correct ? 'text-success' : 'text-danger'">
|
|
<i :class="hour.models['N-BEATS'].prediction === 1 ? 'bi-arrow-up' : 'bi-arrow-down'"></i>
|
|
<i v-if="hour.actual_result !== null" :class="hour.models['N-BEATS'].is_correct ? 'bi-check' : 'bi-x'"></i>
|
|
</span>
|
|
</td>
|
|
|
|
<!-- TTM-R2 -->
|
|
<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>
|
|
<i v-if="hour.actual_result !== null" :class="hour.models['TTM-R2'].is_correct ? 'bi-check' : 'bi-x'"></i>
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="!hourlyModal.data?.length">
|
|
<td colspan="8" 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>
|