Paginate review list (10/page); show open/close/hold duration in BJT.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -247,12 +247,10 @@ def auth_token(conn: sqlite3.Connection, token: str) -> Optional[dict]:
|
||||
return row_to_dict(row)
|
||||
|
||||
|
||||
def list_trades(
|
||||
conn: sqlite3.Connection,
|
||||
limit: int = 200,
|
||||
def _trades_where(
|
||||
account_id: Optional[int] = None,
|
||||
machine_id: Optional[int] = None,
|
||||
) -> list[dict]:
|
||||
) -> tuple[str, list[Any]]:
|
||||
where: list[str] = []
|
||||
params: list[Any] = []
|
||||
if account_id is not None:
|
||||
@@ -261,16 +259,39 @@ def list_trades(
|
||||
if machine_id is not None:
|
||||
where.append("t.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
|
||||
FROM trades t
|
||||
LEFT JOIN accounts a ON a.id = t.account_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:
|
||||
sql += " WHERE " + " AND ".join(where)
|
||||
sql += " ORDER BY COALESCE(t.close_time, t.open_time, t.created_at) DESC LIMIT ?"
|
||||
params.append(limit)
|
||||
params.extend([limit, max(0, int(offset))])
|
||||
cur = conn.execute(sql, tuple(params))
|
||||
rows = []
|
||||
for r in cur.fetchall():
|
||||
|
||||
+56
-8
@@ -4,9 +4,11 @@ from __future__ import annotations
|
||||
import json
|
||||
import secrets
|
||||
import time
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from functools import wraps
|
||||
|
||||
BJT = timezone(timedelta(hours=8))
|
||||
|
||||
from flask import (
|
||||
Flask,
|
||||
Response,
|
||||
@@ -186,10 +188,39 @@ def register_routes(app: Flask) -> None:
|
||||
if value is None or value == "":
|
||||
return "—"
|
||||
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):
|
||||
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:
|
||||
with database.get_db() as conn:
|
||||
if u["role"] == "admin":
|
||||
@@ -596,22 +627,38 @@ def register_routes(app: Flask) -> None:
|
||||
u = current_user()
|
||||
machine_raw = (request.args.get("machine_id") or "").strip()
|
||||
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:
|
||||
machines = database.list_machines_for_user(conn, u)
|
||||
allowed_ids = {int(m["id"]) for m in machines}
|
||||
if machine_id is not None and machine_id not in allowed_ids:
|
||||
machine_id = None
|
||||
if u["role"] == "admin":
|
||||
trades = database.list_trades(conn, 300, machine_id=machine_id)
|
||||
else:
|
||||
trades = database.list_trades(
|
||||
conn, 300, account_id=u.get("account_id"), machine_id=machine_id
|
||||
)
|
||||
total = database.count_trades(conn, account_id=account_id, machine_id=machine_id)
|
||||
total_pages = max(1, (total + per_page - 1) // per_page)
|
||||
if page > total_pages:
|
||||
page = total_pages
|
||||
offset = (page - 1) * per_page
|
||||
trades = database.list_trades(
|
||||
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(
|
||||
"records.html",
|
||||
trades=trades,
|
||||
machines=machines,
|
||||
selected_machine_id=machine_id,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
total=total,
|
||||
total_pages=total_pages,
|
||||
)
|
||||
|
||||
@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")):
|
||||
flash("记录不存在或无权查看", "error")
|
||||
return redirect(url_for("records_page"))
|
||||
_annotate_trade_times(trade)
|
||||
center = trade.get("open_time") or trade.get("created_at") or database.now()
|
||||
window = 3600 * 6
|
||||
charts = {}
|
||||
|
||||
@@ -11,8 +11,11 @@
|
||||
<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.volume }}</td></tr>
|
||||
<tr><th>开仓</th><td>{{ trade.open_price }} / {{ trade.open_time }}</td></tr>
|
||||
<tr><th>平仓</th><td>{{ trade.close_price }} / {{ trade.close_time }}</td></tr>
|
||||
<tr><th>开仓价</th><td>{{ trade.open_price }}</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>SL / TP</th><td>{{ trade.sl }} / {{ trade.tp }}</td></tr>
|
||||
<tr><th>单号</th><td>{{ trade.ticket or '—' }}</td></tr>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
{% block title %}交易复盘 · {{ app_name }}{% endblock %}
|
||||
{% block content %}
|
||||
<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">
|
||||
<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>
|
||||
<select name="machine_id" id="machine_id" style="min-width:200px">
|
||||
<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 %}>
|
||||
@@ -15,20 +15,33 @@
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="hidden" name="page" value="1">
|
||||
<button type="submit" class="btn">筛选</button>
|
||||
{% if selected_machine_id %}
|
||||
<a class="btn secondary" href="{{ url_for('records_page') }}">清除</a>
|
||||
{% endif %}
|
||||
<span class="muted">共 {{ trades|length }} 条</span>
|
||||
<span class="muted">共 {{ total }} 条 · 第 {{ page }}/{{ total_pages }} 页</span>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card" style="overflow-x:auto">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th><th>品种</th><th>方向</th><th>手数</th><th>开仓价</th><th>平仓价</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>
|
||||
</tr>
|
||||
</thead>
|
||||
<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 %}">
|
||||
{{ t.profit if t.profit is not none else '—' }}
|
||||
</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.machine_name or '—' }}</td>
|
||||
<td>{{ t.entry_style or '—' }}</td>
|
||||
<td><a href="{{ url_for('record_detail', tid=t.id) }}">打开复盘</a></td>
|
||||
</tr>
|
||||
{% 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 %}
|
||||
</tbody>
|
||||
</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>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user