重构统计分析页:汇总指标、分项下拉与后台缓存

新增 stats_engine 与 stats_cache,提供 API 自动加载 8 种统计维度;交易与复盘变更时自动刷新缓存。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-15 16:46:06 +08:00
parent 0e385b057d
commit e8b4dbbaca
4 changed files with 581 additions and 179 deletions
+47 -111
View File
@@ -2,122 +2,58 @@
{% block title %}统计分析 - 国内期货监控系统{% endblock %}
{% block content %}
<div class="stat-grid">
<div class="stat-item"><div class="label">总交易</div><div class="value">{{ total }}</div></div>
<div class="stat-item"><div class="label">止盈</div><div class="value text-profit">{{ win }}</div></div>
<div class="stat-item"><div class="label">止损</div><div class="value text-loss">{{ loss }}</div></div>
<div class="stat-item"><div class="label">胜率</div><div class="value">{{ rate }}%</div></div>
<div class="stats-toolbar">
<span id="stats-updated" class="hint">正在加载统计…</span>
<button type="button" class="btn-secondary" id="stats-refresh-btn">重新计算</button>
</div>
<div class="stat-grid">
<div class="stat-item"><div class="label">累计手续费</div><div class="value text-loss">{{ total_fee }} 元</div></div>
<div class="stat-item"><div class="label">毛盈亏合计</div><div class="value">{{ total_gross }} 元</div></div>
<div class="stat-item"><div class="label">净盈亏合计</div><div class="value {% if total_net > 0 %}text-profit{% elif total_net < 0 %}text-loss{% endif %}">{{ total_net }} 元</div></div>
<div class="stat-item"><div class="label">计费笔数</div><div class="value">{{ fee_count }}</div></div>
<div class="stat-grid stat-grid-summary" id="stats-summary">
<div class="stat-item"><div class="label">总交易次数</div><div class="value" data-k="total_trades">-</div></div>
<div class="stat-item"><div class="label">胜率</div><div class="value" data-k="win_rate">-</div></div>
<div class="stat-item"><div class="label">平均盈利</div><div class="value text-profit" data-k="avg_profit">-</div></div>
<div class="stat-item"><div class="label">平均亏损</div><div class="value text-loss" data-k="avg_loss">-</div></div>
<div class="stat-item"><div class="label">盈亏比</div><div class="value" data-k="profit_loss_ratio">-</div></div>
<div class="stat-item"><div class="label">连续亏损次数</div><div class="value" data-k="consecutive_losses">-</div></div>
<div class="stat-item"><div class="label">最大回撤</div><div class="value" data-k="max_drawdown">-</div></div>
<div class="stat-item"><div class="label">最大亏损金额</div><div class="value text-loss" data-k="max_loss_amount">-</div></div>
<div class="stat-item"><div class="label">最大亏损占比</div><div class="value text-loss" data-k="max_loss_pct">-</div></div>
<div class="stat-item"><div class="label">最大盈利金额</div><div class="value text-profit" data-k="max_profit_amount">-</div></div>
<div class="stat-item"><div class="label">最大盈利占比</div><div class="value text-profit" data-k="max_profit_pct">-</div></div>
<div class="stat-item"><div class="label">累计手续费</div><div class="value text-loss" data-k="total_fee">-</div></div>
<div class="stat-item"><div class="label">情绪单数量</div><div class="value" data-k="emotion_count">-</div></div>
<div class="stat-item"><div class="label">情绪单占比</div><div class="value" data-k="emotion_ratio">-</div></div>
</div>
<div class="card">
<h2>按品种统计</h2>
<table>
<thead><tr><th>品种</th><th>交易次数</th><th>止盈次数</th><th>胜率</th></tr></thead>
<tbody>
{% for s in by_symbol %}
<tr>
<td>{{ s.symbol_name or s.symbol }}</td>
<td>{{ s.cnt }}</td>
<td>{{ s.wins }}</td>
<td>{{ round(s.wins / s.cnt * 100, 2) if s.cnt else 0 }}%</td>
</tr>
{% else %}
<tr><td colspan="4" class="text-muted">暂无数据</td></tr>
{% endfor %}
</tbody>
</table>
<div class="stats-card-head">
<h2>分项统计</h2>
<div class="field stats-view-field">
<label for="stats-view-select">统计维度</label>
<select id="stats-view-select"></select>
</div>
</div>
<div class="card-scroll">
<table id="stats-breakdown-table">
<thead><tr id="stats-breakdown-head"></tr></thead>
<tbody id="stats-breakdown-body">
<tr><td colspan="12" class="text-muted">加载中…</td></tr>
</tbody>
</table>
</div>
</div>
<div class="card">
<h2>手续费按品种(交易记录)</h2>
<table>
<thead><tr><th>品种</th><th>笔数</th><th>累计手续费(元)</th></tr></thead>
<tbody>
{% for f in fee_by_symbol %}
<tr>
<td>{{ f.symbol_name or f.symbol }}</td>
<td>{{ f.cnt }}</td>
<td class="text-loss">{{ round(f.total_fee, 2) }}</td>
</tr>
{% else %}
<tr><td colspan="3" class="text-muted">暂无手续费数据</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card">
<h2>按类型统计</h2>
<table>
<thead><tr><th>类型</th><th>交易次数</th><th>止盈次数</th><th>胜率</th></tr></thead>
<tbody>
{% for t in by_type %}
<tr>
<td>{{ t.monitor_type }}</td>
<td>{{ t.cnt }}</td>
<td>{{ t.wins }}</td>
<td>{{ round(t.wins / t.cnt * 100, 2) if t.cnt else 0 }}%</td>
</tr>
{% else %}
<tr><td colspan="4" class="text-muted">暂无数据</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card">
<h2>按方向统计</h2>
<table>
<thead><tr><th>方向</th><th>交易次数</th><th>止盈次数</th><th>胜率</th></tr></thead>
<tbody>
{% for d in by_direction %}
<tr>
<td><span class="badge dir">{{ '做多' if d.direction == 'long' else '做空' }}</span></td>
<td>{{ d.cnt }}</td>
<td>{{ d.wins }}</td>
<td>{{ round(d.wins / d.cnt * 100, 2) if d.cnt else 0 }}%</td>
</tr>
{% else %}
<tr><td colspan="4" class="text-muted">暂无数据</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card">
<h2>最近 10 笔交易记录</h2>
<table>
<thead><tr><th>品种</th><th>方向</th><th>毛盈亏</th><th>手续费</th><th>净盈亏</th><th>结果</th><th>时间</th></tr></thead>
<tbody>
{% for r in recent %}
<tr>
<td>{{ r.symbol_name or r.symbol }}</td>
<td><span class="badge dir">{{ '做多' if r.direction == 'long' else '做空' }}</span></td>
<td>{{ r.pnl if r.pnl is not none else '-' }}</td>
<td class="text-muted">{{ r.fee if r.fee is not none else '-' }}</td>
<td>
{% if r.pnl_net and r.pnl_net > 0 %}<span class="badge profit">{{ r.pnl_net }}</span>
{% elif r.pnl_net and r.pnl_net < 0 %}<span class="badge loss">{{ r.pnl_net }}</span>
{% else %}-{% endif %}
</td>
<td>
{% if r.result == '止盈' %}<span class="badge profit">止盈</span>
{% elif r.result == '止损' %}<span class="badge loss">止损</span>
{% else %}{{ r.result or '-' }}{% endif %}
</td>
<td>{{ r.close_time[:16] if r.close_time else (r.created_at[:16] if r.created_at else '') }}</td>
</tr>
{% else %}
<tr><td colspan="7" class="text-muted">暂无数据</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<style>
.stats-toolbar{display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-bottom:1rem;flex-wrap:wrap}
.stats-toolbar .btn-secondary{width:auto;padding:.5rem 1rem;border-radius:8px;border:1px solid var(--input-border);background:var(--toggle-bg);color:var(--text-primary);cursor:pointer;font-size:.85rem}
.stats-toolbar .btn-secondary:hover{border-color:var(--accent);color:var(--accent)}
.stat-grid-summary{grid-template-columns:repeat(auto-fill,minmax(128px,1fr))}
.stats-card-head{display:flex;align-items:flex-end;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin-bottom:1rem}
.stats-card-head h2{margin-bottom:0}
.stats-view-field{width:auto;min-width:200px}
.stats-view-field select{width:100%;min-width:180px}
</style>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/stats.js') }}"></script>
{% endblock %}