diff --git a/app/db.py b/app/db.py index 7a0ce1e..159735f 100644 --- a/app/db.py +++ b/app/db.py @@ -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(): diff --git a/app/routes.py b/app/routes.py index aceb357..8283e56 100644 --- a/app/routes.py +++ b/app/routes.py @@ -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/") @@ -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 = {} diff --git a/app/templates/record_detail.html b/app/templates/record_detail.html index e2f075f..bf158b6 100644 --- a/app/templates/record_detail.html +++ b/app/templates/record_detail.html @@ -11,8 +11,11 @@ 账户{{ trade.account_name or '—' }} 机器{{ trade.machine_name or '—' }} 手数{{ trade.volume }} - 开仓{{ trade.open_price }} / {{ trade.open_time }} - 平仓{{ trade.close_price }} / {{ trade.close_time }} + 开仓价{{ trade.open_price }} + 平仓价{{ trade.close_price if trade.close_price is not none else '—' }} + 开仓时间{{ trade.open_time_fmt }} 北京时间 + 平仓时间{{ trade.close_time_fmt }} 北京时间 + 持仓时间{{ trade.hold_duration }} 盈亏{{ trade.profit }} SL / TP{{ trade.sl }} / {{ trade.tp }} 单号{{ trade.ticket or '—' }} diff --git a/app/templates/records.html b/app/templates/records.html index e5874ea..cd74df0 100644 --- a/app/templates/records.html +++ b/app/templates/records.html @@ -2,12 +2,12 @@ {% block title %}交易复盘 · {{ app_name }}{% endblock %} {% block content %}

交易记录与复盘

-

列表可按机器筛选 → 点开详情 → 四周期 K 线 + 入场形式标注。

+

按机器筛选 · 每页 {{ per_page }} 条 · 时间均为北京时间

- - {% for m in machines %} {% endfor %} + {% if selected_machine_id %} 清除 {% endif %} - 共 {{ trades|length }} 条 + 共 {{ total }} 条 · 第 {{ page }}/{{ total_pages }} 页
-
+
- - + + + + + + + + + + + + + + @@ -43,15 +56,36 @@ + + + {% else %} - + {% endfor %}
ID品种方向手数开仓价平仓价盈亏账户机器入场形式ID品种方向手数开仓价平仓价盈亏开仓时间平仓时间持仓时间账户机器入场形式
{{ t.profit if t.profit is not none else '—' }} {{ t.open_time_fmt }}{{ t.close_time_fmt }}{{ t.hold_duration }} {{ t.account_name or '—' }} {{ t.machine_name or '—' }} {{ t.entry_style or '—' }} 打开复盘
暂无数据。{% if selected_machine_id %}当前机器下没有成交。{% else %}客户端上报成交后出现在此。{% endif %}
暂无数据。{% if selected_machine_id %}当前机器下没有成交。{% else %}客户端上报成交后出现在此。{% endif %}
+ + {% if total_pages > 1 %} + {% set q = {'page': page} %} + {% if selected_machine_id %}{% set _ = q.update({'machine_id': selected_machine_id}) %}{% endif %} +
+ {% if page > 1 %} + 上一页 + {% else %} + 上一页 + {% endif %} + {{ page }} / {{ total_pages }} + {% if page < total_pages %} + 下一页 + {% else %} + 下一页 + {% endif %} +
+ {% endif %}
{% endblock %}