Paginate review list (10/page); show open/close/hold duration in BJT.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-17 22:21:12 +08:00
parent 3164a21348
commit dbfddeedcb
4 changed files with 133 additions and 27 deletions
+30 -9
View File
@@ -247,12 +247,10 @@ def auth_token(conn: sqlite3.Connection, token: str) -> Optional[dict]:
return row_to_dict(row) return row_to_dict(row)
def list_trades( def _trades_where(
conn: sqlite3.Connection,
limit: int = 200,
account_id: Optional[int] = None, account_id: Optional[int] = None,
machine_id: Optional[int] = None, machine_id: Optional[int] = None,
) -> list[dict]: ) -> tuple[str, list[Any]]:
where: list[str] = [] where: list[str] = []
params: list[Any] = [] params: list[Any] = []
if account_id is not None: if account_id is not None:
@@ -261,16 +259,39 @@ def list_trades(
if machine_id is not None: if machine_id is not None:
where.append("t.machine_id=?") where.append("t.machine_id=?")
params.append(machine_id) params.append(machine_id)
sql = """ clause = (" WHERE " + " AND ".join(where)) if where else ""
return clause, params
def count_trades(
conn: sqlite3.Connection,
account_id: Optional[int] = None,
machine_id: Optional[int] = None,
) -> int:
clause, params = _trades_where(account_id, machine_id)
sql = f"SELECT COUNT(*) AS c FROM trades t{clause}"
row = conn.execute(sql, tuple(params)).fetchone()
return int(row["c"] if row else 0)
def list_trades(
conn: sqlite3.Connection,
limit: int = 200,
account_id: Optional[int] = None,
machine_id: Optional[int] = None,
offset: int = 0,
) -> list[dict]:
clause, params = _trades_where(account_id, machine_id)
sql = f"""
SELECT t.*, a.name AS account_name, m.name AS machine_name SELECT t.*, a.name AS account_name, m.name AS machine_name
FROM trades t FROM trades t
LEFT JOIN accounts a ON a.id = t.account_id LEFT JOIN accounts a ON a.id = t.account_id
LEFT JOIN machines m ON m.id = t.machine_id LEFT JOIN machines m ON m.id = t.machine_id
{clause}
ORDER BY COALESCE(t.close_time, t.open_time, t.created_at) DESC
LIMIT ? OFFSET ?
""" """
if where: params.extend([limit, max(0, int(offset))])
sql += " WHERE " + " AND ".join(where)
sql += " ORDER BY COALESCE(t.close_time, t.open_time, t.created_at) DESC LIMIT ?"
params.append(limit)
cur = conn.execute(sql, tuple(params)) cur = conn.execute(sql, tuple(params))
rows = [] rows = []
for r in cur.fetchall(): for r in cur.fetchall():
+54 -6
View File
@@ -4,9 +4,11 @@ from __future__ import annotations
import json import json
import secrets import secrets
import time import time
from datetime import datetime from datetime import datetime, timedelta, timezone
from functools import wraps from functools import wraps
BJT = timezone(timedelta(hours=8))
from flask import ( from flask import (
Flask, Flask,
Response, Response,
@@ -186,10 +188,39 @@ def register_routes(app: Flask) -> None:
if value is None or value == "": if value is None or value == "":
return "—" return "—"
try: try:
return datetime.fromtimestamp(float(value)).strftime("%Y-%m-%d %H:%M:%S") return datetime.fromtimestamp(float(value), BJT).strftime("%Y-%m-%d %H:%M:%S")
except (TypeError, ValueError, OSError): except (TypeError, ValueError, OSError):
return str(value) return str(value)
def _fmt_hold(open_ts, close_ts) -> str:
"""持仓时长:平仓−开仓;未平仓则按当前时间估算。"""
if open_ts is None or open_ts == "":
return "—"
try:
start = float(open_ts)
end = float(close_ts) if close_ts not in (None, "") else time.time()
except (TypeError, ValueError):
return "—"
secs = max(0, int(end - start))
days, rem = divmod(secs, 86400)
hours, rem = divmod(rem, 3600)
mins, sec = divmod(rem, 60)
if days > 0:
return f"{days}天{hours}小时{mins}分"
if hours > 0:
return f"{hours}小时{mins}分"
if mins > 0:
return f"{mins}分{sec}秒"
return f"{sec}秒"
def _annotate_trade_times(t: dict) -> None:
t["open_time_fmt"] = _fmt_ts(t.get("open_time"))
t["close_time_fmt"] = _fmt_ts(t.get("close_time"))
closed = t.get("close_time") not in (None, "")
t["hold_duration"] = _fmt_hold(t.get("open_time"), t.get("close_time"))
if not closed and t.get("open_time"):
t["hold_duration"] = t["hold_duration"] + "(持仓中)"
def _dashboard_payload(u: dict) -> dict: def _dashboard_payload(u: dict) -> dict:
with database.get_db() as conn: with database.get_db() as conn:
if u["role"] == "admin": if u["role"] == "admin":
@@ -596,22 +627,38 @@ def register_routes(app: Flask) -> None:
u = current_user() u = current_user()
machine_raw = (request.args.get("machine_id") or "").strip() machine_raw = (request.args.get("machine_id") or "").strip()
machine_id = int(machine_raw) if machine_raw.isdigit() else None machine_id = int(machine_raw) if machine_raw.isdigit() else None
page_raw = (request.args.get("page") or "1").strip()
page = int(page_raw) if page_raw.isdigit() and int(page_raw) >= 1 else 1
per_page = 10
account_id = None if u["role"] == "admin" else u.get("account_id")
with database.get_db() as conn: with database.get_db() as conn:
machines = database.list_machines_for_user(conn, u) machines = database.list_machines_for_user(conn, u)
allowed_ids = {int(m["id"]) for m in machines} allowed_ids = {int(m["id"]) for m in machines}
if machine_id is not None and machine_id not in allowed_ids: if machine_id is not None and machine_id not in allowed_ids:
machine_id = None machine_id = None
if u["role"] == "admin": total = database.count_trades(conn, account_id=account_id, machine_id=machine_id)
trades = database.list_trades(conn, 300, machine_id=machine_id) total_pages = max(1, (total + per_page - 1) // per_page)
else: if page > total_pages:
page = total_pages
offset = (page - 1) * per_page
trades = database.list_trades( trades = database.list_trades(
conn, 300, account_id=u.get("account_id"), machine_id=machine_id conn,
limit=per_page,
account_id=account_id,
machine_id=machine_id,
offset=offset,
) )
for t in trades:
_annotate_trade_times(t)
return render_template( return render_template(
"records.html", "records.html",
trades=trades, trades=trades,
machines=machines, machines=machines,
selected_machine_id=machine_id, selected_machine_id=machine_id,
page=page,
per_page=per_page,
total=total,
total_pages=total_pages,
) )
@app.get("/records/<int:tid>") @app.get("/records/<int:tid>")
@@ -623,6 +670,7 @@ def register_routes(app: Flask) -> None:
if not trade or not database.user_can_access_account(u, trade.get("account_id")): if not trade or not database.user_can_access_account(u, trade.get("account_id")):
flash("记录不存在或无权查看", "error") flash("记录不存在或无权查看", "error")
return redirect(url_for("records_page")) return redirect(url_for("records_page"))
_annotate_trade_times(trade)
center = trade.get("open_time") or trade.get("created_at") or database.now() center = trade.get("open_time") or trade.get("created_at") or database.now()
window = 3600 * 6 window = 3600 * 6
charts = {} charts = {}
+5 -2
View File
@@ -11,8 +11,11 @@
<tr><th>账户</th><td>{{ trade.account_name or '—' }}</td></tr> <tr><th>账户</th><td>{{ trade.account_name or '—' }}</td></tr>
<tr><th>机器</th><td>{{ trade.machine_name or '—' }}</td></tr> <tr><th>机器</th><td>{{ trade.machine_name or '—' }}</td></tr>
<tr><th>手数</th><td>{{ trade.volume }}</td></tr> <tr><th>手数</th><td>{{ trade.volume }}</td></tr>
<tr><th>开仓</th><td>{{ trade.open_price }} / {{ trade.open_time }}</td></tr> <tr><th>开仓价</th><td>{{ trade.open_price }}</td></tr>
<tr><th>平仓</th><td>{{ trade.close_price }} / {{ trade.close_time }}</td></tr> <tr><th>平仓价</th><td>{{ trade.close_price if trade.close_price is not none else '—' }}</td></tr>
<tr><th>开仓时间</th><td>{{ trade.open_time_fmt }} <span class="muted">北京时间</span></td></tr>
<tr><th>平仓时间</th><td>{{ trade.close_time_fmt }} <span class="muted">北京时间</span></td></tr>
<tr><th>持仓时间</th><td style="font-weight:700;color:var(--accent)">{{ trade.hold_duration }}</td></tr>
<tr><th>盈亏</th><td>{{ trade.profit }}</td></tr> <tr><th>盈亏</th><td>{{ trade.profit }}</td></tr>
<tr><th>SL / TP</th><td>{{ trade.sl }} / {{ trade.tp }}</td></tr> <tr><th>SL / TP</th><td>{{ trade.sl }} / {{ trade.tp }}</td></tr>
<tr><th>单号</th><td>{{ trade.ticket or '—' }}</td></tr> <tr><th>单号</th><td>{{ trade.ticket or '—' }}</td></tr>
+42 -8
View File
@@ -2,12 +2,12 @@
{% 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">交易记录与复盘</h2>
<p class="muted" style="margin:0 0 14px">列表可按机器筛选 → 点开详情 → 四周期 K 线 + 入场形式标注。</p> <p class="muted" style="margin:0 0 14px">按机器筛选 · 每页 {{ per_page }} 条 · 时间均为北京时间</p>
<div class="card" style="margin-bottom:14px"> <div class="card" style="margin-bottom:14px">
<form method="get" action="{{ url_for('records_page') }}" style="display:flex;gap:10px;align-items:center;flex-wrap:wrap"> <form method="get" action="{{ url_for('records_page') }}" style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
<label class="muted" for="machine_id">机器</label> <label class="muted" for="machine_id" style="margin:0">机器</label>
<select name="machine_id" id="machine_id" style="min-width:200px"> <select name="machine_id" id="machine_id" style="min-width:200px;width:auto">
<option value="">全部机器</option> <option value="">全部机器</option>
{% for m in machines %} {% for m in machines %}
<option value="{{ m.id }}" {% if selected_machine_id == m.id %}selected{% endif %}> <option value="{{ m.id }}" {% if selected_machine_id == m.id %}selected{% endif %}>
@@ -15,20 +15,33 @@
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
<input type="hidden" name="page" value="1">
<button type="submit" class="btn">筛选</button> <button type="submit" class="btn">筛选</button>
{% if selected_machine_id %} {% if selected_machine_id %}
<a class="btn secondary" href="{{ url_for('records_page') }}">清除</a> <a class="btn secondary" href="{{ url_for('records_page') }}">清除</a>
{% endif %} {% endif %}
<span class="muted">共 {{ trades|length }} 条</span> <span class="muted">共 {{ total }} 条 · 第 {{ page }}/{{ total_pages }} 页</span>
</form> </form>
</div> </div>
<div class="card"> <div class="card" style="overflow-x:auto">
<table> <table>
<thead> <thead>
<tr> <tr>
<th>ID</th><th>品种</th><th>方向</th><th>手数</th><th>开仓价</th><th>平仓价</th> <th>ID</th>
<th>盈亏</th><th>账户</th><th>机器</th><th>入场形式</th><th></th> <th>品种</th>
<th>方向</th>
<th>手数</th>
<th>开仓价</th>
<th>平仓价</th>
<th>盈亏</th>
<th>开仓时间</th>
<th>平仓时间</th>
<th>持仓时间</th>
<th>账户</th>
<th>机器</th>
<th>入场形式</th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -43,15 +56,36 @@
<td style="color:{% if t.profit and t.profit>0 %}var(--green){% elif t.profit and t.profit<0 %}var(--red){% else %}inherit{% endif %}"> <td style="color:{% if t.profit and t.profit>0 %}var(--green){% elif t.profit and t.profit<0 %}var(--red){% else %}inherit{% endif %}">
{{ t.profit if t.profit is not none else '—' }} {{ t.profit if t.profit is not none else '—' }}
</td> </td>
<td class="muted" style="white-space:nowrap">{{ t.open_time_fmt }}</td>
<td class="muted" style="white-space:nowrap">{{ t.close_time_fmt }}</td>
<td style="white-space:nowrap;font-weight:600;color:var(--accent)">{{ t.hold_duration }}</td>
<td>{{ t.account_name or '—' }}</td> <td>{{ t.account_name or '—' }}</td>
<td>{{ t.machine_name or '—' }}</td> <td>{{ t.machine_name or '—' }}</td>
<td>{{ t.entry_style or '—' }}</td> <td>{{ t.entry_style or '—' }}</td>
<td><a href="{{ url_for('record_detail', tid=t.id) }}">打开复盘</a></td> <td><a href="{{ url_for('record_detail', tid=t.id) }}">打开复盘</a></td>
</tr> </tr>
{% else %} {% else %}
<tr><td colspan="11" class="muted">暂无数据。{% if selected_machine_id %}当前机器下没有成交。{% else %}客户端上报成交后出现在此。{% endif %}</td></tr> <tr><td colspan="14" class="muted">暂无数据。{% if selected_machine_id %}当前机器下没有成交。{% else %}客户端上报成交后出现在此。{% endif %}</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if total_pages > 1 %}
{% set q = {'page': page} %}
{% if selected_machine_id %}{% set _ = q.update({'machine_id': selected_machine_id}) %}{% endif %}
<div style="display:flex;gap:10px;align-items:center;justify-content:flex-end;margin-top:14px;flex-wrap:wrap">
{% if page > 1 %}
<a class="btn secondary" href="{{ url_for('records_page', page=page-1, machine_id=selected_machine_id) if selected_machine_id else url_for('records_page', page=page-1) }}">上一页</a>
{% else %}
<span class="btn secondary" style="opacity:.4;pointer-events:none">上一页</span>
{% endif %}
<span class="muted">{{ page }} / {{ total_pages }}</span>
{% if page < total_pages %}
<a class="btn secondary" href="{{ url_for('records_page', page=page+1, machine_id=selected_machine_id) if selected_machine_id else url_for('records_page', page=page+1) }}">下一页</a>
{% else %}
<span class="btn secondary" style="opacity:.4;pointer-events:none">下一页</span>
{% endif %}
</div>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}