Auto-refresh review list and stats when trades or annotations change.

Clarify that category/style breakdowns need manual entry tagging.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-23 13:28:27 +08:00
parent c16dc2e061
commit e15b1d7491
4 changed files with 108 additions and 5 deletions
+38 -1
View File
@@ -398,6 +398,43 @@ def count_trades(
return int(row["c"] if row else 0) return int(row["c"] if row else 0)
def trades_fingerprint(
conn: sqlite3.Connection,
account_id: Optional[int] = None,
machine_id: Optional[int] = None,
) -> str:
"""列表/统计页轮询用:成交条数、最新 id/平仓时间、标注变化任一变即刷新。"""
clause, params = _trades_where(account_id, machine_id, hide_synthetic=True)
row = conn.execute(
f"""
SELECT COUNT(*) AS c,
COALESCE(MAX(t.id), 0) AS max_id,
COALESCE(MAX(t.close_time), 0) AS max_close,
COALESCE(MAX(t.open_time), 0) AS max_open,
SUM(CASE WHEN IFNULL(t.entry_style,'') != '' THEN 1 ELSE 0 END) AS annotated,
SUM(CASE WHEN IFNULL(t.review_issues,'') != '' THEN 1 ELSE 0 END) AS issues,
SUM(CASE WHEN t.close_time IS NULL OR t.close_time='' THEN 1 ELSE 0 END) AS open_n
FROM trades t
{clause}
""",
tuple(params),
).fetchone()
if not row:
return "0"
return "|".join(
str(x)
for x in (
int(row["c"] or 0),
int(row["max_id"] or 0),
round(float(row["max_close"] or 0), 3),
round(float(row["max_open"] or 0), 3),
int(row["annotated"] or 0),
int(row["issues"] or 0),
int(row["open_n"] or 0),
)
)
def list_trades( def list_trades(
conn: sqlite3.Connection, conn: sqlite3.Connection,
limit: int = 200, limit: int = 200,
@@ -792,7 +829,7 @@ def trade_stats(
"by_style": [ "by_style": [
_finalize_group(by_style[k]) _finalize_group(by_style[k])
for k in style_order for k in style_order
if k in by_style and (by_style[k]["count"] or k in ENTRY_STYLES) if k in by_style and by_style[k]["count"]
], ],
} }
+14
View File
@@ -941,6 +941,20 @@ def register_routes(app: Flask) -> None:
total_pages=total_pages, total_pages=total_pages,
) )
@app.get("/api/records/fingerprint")
@login_required
def records_fingerprint():
"""复盘列表/统计页轮询:成交或标注变化时前端自动刷新。"""
u = current_user()
machine_raw = (request.args.get("machine_id") or "").strip()
account_id = None if u["role"] == "admin" else u.get("account_id")
_machines, machine_id = _resolve_machine_filter(u, machine_raw)
with database.get_db() as conn:
fp = database.trades_fingerprint(
conn, account_id=account_id, machine_id=machine_id
)
return jsonify({"ok": True, "fp": fp, "ts": database.now()})
@app.get("/records/stats") @app.get("/records/stats")
@login_required @login_required
def records_stats_page(): def records_stats_page():
+5 -2
View File
@@ -1,8 +1,11 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}交易复盘 · {{ app_name }}{% endblock %} {% block title %}交易复盘 · {{ app_name }}{% endblock %}
{% block content %} {% block content %}
<h2 style="margin:0 0 6px">交易记录与复盘</h2> <h2 style="margin:0 0 6px;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<p class="muted" style="margin:0 0 10px">按机器筛选 · 每页 {{ per_page }} 条 · 时间均为北京时间 · 已隐藏对账/去重影子记录</p> 交易记录与复盘
<span id="records-live-hint" class="tag" style="font-size:12px;font-weight:500">自动刷新中…</span>
</h2>
<p class="muted" style="margin:0 0 10px">按机器筛选 · 每页 {{ per_page }} 条 · 北京时间 · 成交变化会自动刷新 · 已隐藏对账/去重影子记录</p>
<div style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap"> <div style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap">
<a class="btn" 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_page', machine_id=selected_machine_id) if selected_machine_id else url_for('records_page') }}">交易列表</a>
+51 -2
View File
@@ -1,9 +1,13 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}统计看板 · {{ app_name }}{% endblock %} {% block title %}统计看板 · {{ app_name }}{% endblock %}
{% block content %} {% block content %}
<h2 style="margin:0 0 6px">统计看板</h2> <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"> <p class="muted" style="margin:0 0 10px">
默认按北京时间<strong>今日真实平仓</strong>统计;对账/去重产生的假平仓已排除。 默认按北京时间<strong>今日真实平仓</strong>统计;对账/去重产生的假平仓已排除。
成交/标注变化会<strong>自动刷新</strong>本页。
{% if selected_period == 'today' and stats.day %}当前:{{ stats.day }}{% endif %} {% if selected_period == 'today' and stats.day %}当前:{{ stats.day }}{% endif %}
{% if stats.skipped_synthetic %} · 已排除假平仓 {{ stats.skipped_synthetic }} 笔{% endif %} {% if stats.skipped_synthetic %} · 已排除假平仓 {{ stats.skipped_synthetic }} 笔{% endif %}
{% if stats.skipped_dup_ticket %} · 已排除重复单号 {{ stats.skipped_dup_ticket }} 笔{% endif %} {% if stats.skipped_dup_ticket %} · 已排除重复单号 {{ stats.skipped_dup_ticket }} 笔{% endif %}
@@ -82,6 +86,7 @@
<div class="grid cols-2" style="margin-top:14px"> <div class="grid cols-2" style="margin-top:14px">
<div class="card"> <div class="card">
<h3>按大类(顺势 / 反转 / 其他)</h3> <h3>按大类(顺势 / 反转 / 其他)</h3>
<p class="muted" style="margin:0 0 10px">需在复盘页选「入场形式」后才会分入顺势/反转;未选的汇总在「未标注」。</p>
<table> <table>
<thead> <thead>
<tr> <tr>
@@ -107,6 +112,7 @@
</div> </div>
<div class="card"> <div class="card">
<h3>按入场形式(细项)</h3> <h3>按入场形式(细项)</h3>
<p class="muted" style="margin:0 0 10px">只显示已标注过的形式;全部为「—」时请先打开复盘标注。</p>
<table> <table>
<thead> <thead>
<tr> <tr>
@@ -125,7 +131,7 @@
<td class="muted">{{ g.avg_hold }}</td> <td class="muted">{{ g.avg_hold }}</td>
</tr> </tr>
{% else %} {% else %}
<tr><td colspan="5" class="muted">暂无数据</td></tr> <tr><td colspan="5" class="muted">暂无入场形式标注</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
@@ -239,3 +245,46 @@
</table> </table>
</div> </div>
{% endblock %} {% 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 %}