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:
@@ -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"]
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user