feat: upgrade to multi-model ensemble system

- 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
This commit is contained in:
crypto_predictor committed 2026-02-11 22:13:35 +08:00
1 parent c0a02868ac
commit 2993014fa8
5 files changed
+465 -335

No files matched your search

+131 -77
View File
@@ -74,8 +74,8 @@
</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 class="stat-label">策略</div>
<div class="stat-value small">{{ currentTrade.strategy || 'Ensemble' }}</div>
</div>
</div>
<div class="col-md-3">
@@ -116,6 +116,7 @@
<tr>
<th>时间 (北京)</th>
<th>市场</th>
<th>策略</th>
<th>预测</th>
<th>概率</th>
<th>价格</th>
@@ -128,6 +129,9 @@
<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 }}
@@ -139,7 +143,7 @@
</td>
</tr>
<tr v-if="!tradeHistory.data?.length">
<td colspan="6" class="text-center text-muted py-4">
<td colspan="7" class="text-center text-muted py-4">
暂无数据
</td>
</tr>
@@ -184,66 +188,80 @@
<!-- 当月汇总卡片 -->
<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 模型
<!-- 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="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 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 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-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 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="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 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 v-else class="text-center text-muted py-3">暂无数据</div>
</div>
</div>
</div>
@@ -252,52 +270,61 @@
<!-- 每日准确率表格 -->
<div class="card">
<div class="card-header">
<i class="bi bi-calendar3"></i> 每日准确率
<i class="bi bi-calendar3"></i> 每日准确率概览
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<table class="table table-hover mb-0 text-center">
<thead class="table-light">
<tr>
<th>日期 (北京)</th>
<th>DLinear 预测</th>
<th>DLinear 准确率</th>
<th>TTM-R2 预测</th>
<th>TTM-R2 准确率</th>
<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>{{ day.date }}</td>
<td class="text-start ps-4">{{ day.date }}</td>
<!-- Ensemble -->
<td>
<span v-if="day.DLinear">
{{ day.DLinear.correct }}/{{ day.DLinear.total }}
<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['TTM-R2']">
{{ day['TTM-R2'].correct }}/{{ day['TTM-R2'].total }}
<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> 查看
<i class="bi bi-eye"></i> 详情
</button>
</td>
</tr>
@@ -337,7 +364,7 @@
<!-- 每小时详情弹窗 -->
<div class="modal fade" id="hourlyModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
@@ -346,43 +373,70 @@
<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">
<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>收盘</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.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>{{ 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>
{{ hour.models.DLinear.prediction === 1 ? 'UP' : 'DOWN' }}
<i :class="hour.models.DLinear.is_correct ? 'bi-check-circle' : 'bi-x-circle'"></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>
<span v-else class="text-muted">-</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>
{{ hour.models['TTM-R2'].prediction === 1 ? 'UP' : 'DOWN' }}
<i :class="hour.models['TTM-R2'].is_correct ? 'bi-check-circle' : 'bi-x-circle'"></i>
<i v-if="hour.actual_result !== null" :class="hour.models['TTM-R2'].is_correct ? 'bi-check' : 'bi-x'"></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 colspan="8" class="text-center text-muted py-4">
暂无数据
</td>
</tr>