e15b1d7491
Clarify that category/style breakdowns need manual entry tagging. Co-authored-by: Cursor <cursoragent@cursor.com>
291 lines
11 KiB
HTML
291 lines
11 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}统计看板 · {{ app_name }}{% endblock %}
|
|
{% block content %}
|
|
<h2 style="margin:0 0 6px;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
|
统计看板
|
|
<span id="records-live-hint" class="tag" style="font-size:12px;font-weight:500">自动刷新中…</span>
|
|
</h2>
|
|
<p class="muted" style="margin:0 0 10px">
|
|
默认按北京时间<strong>今日真实平仓</strong>统计;对账/去重产生的假平仓已排除。
|
|
成交/标注变化会<strong>自动刷新</strong>本页。
|
|
{% if selected_period == 'today' and stats.day %}当前:{{ stats.day }}{% endif %}
|
|
{% if stats.skipped_synthetic %} · 已排除假平仓 {{ stats.skipped_synthetic }} 笔{% endif %}
|
|
{% if stats.skipped_dup_ticket %} · 已排除重复单号 {{ stats.skipped_dup_ticket }} 笔{% endif %}
|
|
</p>
|
|
|
|
<div style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap">
|
|
<a class="btn secondary" href="{{ url_for('records_page', machine_id=selected_machine_id) if selected_machine_id else url_for('records_page') }}">交易列表</a>
|
|
<a class="btn" href="{{ url_for('records_stats_page', machine_id=selected_machine_id, period=selected_period, day=selected_day) if selected_machine_id else url_for('records_stats_page', period=selected_period, day=selected_day) }}">统计看板</a>
|
|
</div>
|
|
|
|
<div class="card" style="margin-bottom:14px">
|
|
<form method="get" action="{{ url_for('records_stats_page') }}" style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
|
<label class="muted" for="period" style="margin:0">范围</label>
|
|
<select name="period" id="period" style="min-width:120px;width:auto">
|
|
<option value="today" {% if selected_period=='today' %}selected{% endif %}>今日</option>
|
|
<option value="all" {% if selected_period=='all' %}selected{% endif %}>全部</option>
|
|
</select>
|
|
<label class="muted" for="day" style="margin:0">日期</label>
|
|
<input type="date" name="day" id="day" value="{{ selected_day or '' }}" style="width:auto">
|
|
<label class="muted" for="machine_id" style="margin:0">机器</label>
|
|
<select name="machine_id" id="machine_id" style="min-width:200px;width:auto">
|
|
<option value="">全部机器</option>
|
|
{% for m in machines %}
|
|
<option value="{{ m.id }}" {% if selected_machine_id == m.id %}selected{% endif %}>
|
|
{{ m.name }} #{{ m.id }}{% if m.online %} · 在线{% endif %}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
<button type="submit" class="btn">筛选</button>
|
|
<a class="btn secondary" href="{{ url_for('records_stats_page') }}">今日重置</a>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="grid cols-3">
|
|
<div class="card">
|
|
<div class="muted">成交笔数</div>
|
|
<div class="stat">{{ stats.total }}</div>
|
|
<div class="muted" style="margin-top:6px">已平 {{ stats.closed }} · 持仓中 {{ stats.open_pos }}</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="muted">胜率</div>
|
|
<div class="stat">{{ '%.1f'|format(stats.win_rate) }}%</div>
|
|
<div class="muted" style="margin-top:6px">盈 {{ stats.wins }} · 亏 {{ stats.losses }} · 平 {{ stats.breakeven }}</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="muted">合计盈亏{% if selected_period=='today' %}(今日已平){% endif %}</div>
|
|
<div class="stat" style="color:{% if stats.profit>0 %}var(--green){% elif stats.profit<0 %}var(--red){% else %}inherit{% endif %}">
|
|
{{ '%.2f'|format(stats.profit) }}
|
|
</div>
|
|
<div class="muted" style="margin-top:6px">笔均 {{ '%.2f'|format(stats.avg_profit) }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid cols-3" style="margin-top:14px">
|
|
<div class="card">
|
|
<div class="muted">平均持仓时间</div>
|
|
<div class="stat" style="font-size:22px;color:var(--accent)">{{ stats.avg_hold }}</div>
|
|
<div class="muted" style="margin-top:6px">有开仓时间的样本参与计算</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="muted">盈亏因子</div>
|
|
<div class="stat" style="font-size:22px">
|
|
{% if stats.profit_factor is none %}—
|
|
{% elif stats.profit_factor >= 999 %}∞
|
|
{% else %}{{ '%.2f'|format(stats.profit_factor) }}{% endif %}
|
|
</div>
|
|
<div class="muted" style="margin-top:6px">毛利 {{ '%.2f'|format(stats.gross_win) }} / 毛亏 {{ '%.2f'|format(stats.gross_loss) }}</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="muted">入场形式标注</div>
|
|
<div class="stat" style="font-size:22px">{{ stats.annotated }} <span class="muted" style="font-size:14px">/ {{ stats.total }}</span></div>
|
|
<div class="muted" style="margin-top:6px">未标注 {{ stats.unannotated }} · 问题标注 {{ stats.issue_tagged }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid cols-2" style="margin-top:14px">
|
|
<div class="card">
|
|
<h3>按大类(顺势 / 反转 / 其他)</h3>
|
|
<p class="muted" style="margin:0 0 10px">需在复盘页选「入场形式」后才会分入顺势/反转;未选的汇总在「未标注」。</p>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>大类</th><th>笔数</th><th>胜率</th><th>盈亏</th><th>平均持仓</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for g in stats.by_category %}
|
|
<tr>
|
|
<td>{{ g.label }}</td>
|
|
<td>{{ g.count }}</td>
|
|
<td>{{ '%.1f'|format(g.win_rate) }}%</td>
|
|
<td style="color:{% if g.profit>0 %}var(--green){% elif g.profit<0 %}var(--red){% else %}inherit{% endif %}">
|
|
{{ '%.2f'|format(g.profit) }}
|
|
</td>
|
|
<td class="muted">{{ g.avg_hold }}</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr><td colspan="5" class="muted">暂无数据</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="card">
|
|
<h3>按入场形式(细项)</h3>
|
|
<p class="muted" style="margin:0 0 10px">只显示已标注过的形式;全部为「—」时请先打开复盘标注。</p>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>形式</th><th>笔数</th><th>胜率</th><th>盈亏</th><th>平均持仓</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for g in stats.by_style %}
|
|
<tr>
|
|
<td>{{ g.label }}</td>
|
|
<td>{{ g.count }}</td>
|
|
<td>{{ '%.1f'|format(g.win_rate) }}%</td>
|
|
<td style="color:{% if g.profit>0 %}var(--green){% elif g.profit<0 %}var(--red){% else %}inherit{% endif %}">
|
|
{{ '%.2f'|format(g.profit) }}
|
|
</td>
|
|
<td class="muted">{{ g.avg_hold }}</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr><td colspan="5" class="muted">暂无入场形式标注</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid cols-2" style="margin-top:14px">
|
|
<div class="card">
|
|
<h3>按持仓时间</h3>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>区间</th><th>笔数</th><th>胜率</th><th>盈亏</th><th>平均持仓</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for g in stats.by_hold %}
|
|
<tr>
|
|
<td>{{ g.label }}</td>
|
|
<td>{{ g.count }}</td>
|
|
<td>{{ '%.1f'|format(g.win_rate) }}%</td>
|
|
<td style="color:{% if g.profit>0 %}var(--green){% elif g.profit<0 %}var(--red){% else %}inherit{% endif %}">
|
|
{{ '%.2f'|format(g.profit) }}
|
|
</td>
|
|
<td class="muted">{{ g.avg_hold }}</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr><td colspan="5" class="muted">暂无数据</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="card">
|
|
<h3>按方向</h3>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>方向</th><th>笔数</th><th>胜率</th><th>盈亏</th><th>平均持仓</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for g in stats.by_side %}
|
|
<tr>
|
|
<td>{{ g.label }}</td>
|
|
<td>{{ g.count }}</td>
|
|
<td>{{ '%.1f'|format(g.win_rate) }}%</td>
|
|
<td style="color:{% if g.profit>0 %}var(--green){% elif g.profit<0 %}var(--red){% else %}inherit{% endif %}">
|
|
{{ '%.2f'|format(g.profit) }}
|
|
</td>
|
|
<td class="muted">{{ g.avg_hold }}</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr><td colspan="5" class="muted">暂无数据</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-top:14px">
|
|
<h3>按主要问题</h3>
|
|
<p class="muted" style="margin:0 0 10px">一笔可勾 1~2 个问题,会分别计入对应行。</p>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>问题</th><th>笔数</th><th>胜率</th><th>盈亏</th><th>平均持仓</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for g in stats.by_issue %}
|
|
<tr>
|
|
<td>{{ g.label }}</td>
|
|
<td>{{ g.count }}</td>
|
|
<td>{{ '%.1f'|format(g.win_rate) }}%</td>
|
|
<td style="color:{% if g.profit>0 %}var(--green){% elif g.profit<0 %}var(--red){% else %}inherit{% endif %}">
|
|
{{ '%.2f'|format(g.profit) }}
|
|
</td>
|
|
<td class="muted">{{ g.avg_hold }}</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr><td colspan="5" class="muted">暂无问题标注</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card" style="margin-top:14px">
|
|
<h3>按机器</h3>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>机器</th><th>笔数</th><th>胜率</th><th>盈亏</th><th>笔均</th><th>平均持仓</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for g in stats.by_machine %}
|
|
<tr>
|
|
<td>{{ g.label }}</td>
|
|
<td>{{ g.count }}</td>
|
|
<td>{{ '%.1f'|format(g.win_rate) }}%</td>
|
|
<td style="color:{% if g.profit>0 %}var(--green){% elif g.profit<0 %}var(--red){% else %}inherit{% endif %}">
|
|
{{ '%.2f'|format(g.profit) }}
|
|
</td>
|
|
<td>{{ '%.2f'|format(g.avg_profit) }}</td>
|
|
<td class="muted">{{ g.avg_hold }}</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr><td colspan="6" class="muted">暂无数据</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
(function () {
|
|
var fpUrl = {{ url_for('records_fingerprint')|tojson }};
|
|
var machineId = {{ (selected_machine_id or '')|tojson }};
|
|
var hint = document.getElementById('records-live-hint');
|
|
var lastFp = null;
|
|
var q = machineId ? ('?machine_id=' + encodeURIComponent(machineId)) : '';
|
|
|
|
function setHint(text, kind) {
|
|
if (!hint) return;
|
|
hint.textContent = text;
|
|
hint.className = 'tag' + (kind ? ' ' + kind : '');
|
|
}
|
|
|
|
function tick() {
|
|
fetch(fpUrl + q, { credentials: 'same-origin' })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (d) {
|
|
if (!d || !d.ok) return;
|
|
if (lastFp === null) {
|
|
lastFp = d.fp;
|
|
setHint('自动刷新 · 监听中', 'green');
|
|
return;
|
|
}
|
|
if (d.fp !== lastFp) {
|
|
setHint('有新成交,刷新中…', 'green');
|
|
location.reload();
|
|
return;
|
|
}
|
|
setHint('自动刷新 · 监听中', 'green');
|
|
})
|
|
.catch(function () {
|
|
setHint('自动刷新暂时不可用', '');
|
|
});
|
|
}
|
|
|
|
tick();
|
|
setInterval(tick, 3000);
|
|
})();
|
|
</script>
|
|
{% endblock %}
|