From f1e95afb89a13742a2198f68032862e873122ddb Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 4 Jun 2026 10:55:34 +0800 Subject: [PATCH] feat: strategy records dual panels with filters and 100-row cap Split trend and roll snapshot lists with expandable rows, client filters, and DB prune to latest 100 entries. Co-authored-by: Cursor --- strategy_records_register.py | 19 +- strategy_snapshot_lib.py | 134 +++++++- strategy_templates/strategy_records_page.html | 321 ++++++++++++++---- 3 files changed, 402 insertions(+), 72 deletions(-) diff --git a/strategy_records_register.py b/strategy_records_register.py index 67fbf03..0df37ff 100644 --- a/strategy_records_register.py +++ b/strategy_records_register.py @@ -6,12 +6,23 @@ from typing import Any from flask import flash, redirect, url_for -from strategy_snapshot_lib import list_strategy_snapshots +from strategy_snapshot_lib import ( + STRATEGY_SNAPSHOTS_MAX_ROWS, + list_strategy_snapshots_split, +) -def load_strategy_records_page(conn, *, limit: int = 200) -> dict[str, Any]: - snaps = list_strategy_snapshots(conn, limit=limit) - return {"strategy_snapshots": snaps, "strategy_records_limit": limit} +def load_strategy_records_page( + conn, *, limit: int = STRATEGY_SNAPSHOTS_MAX_ROWS +) -> dict[str, Any]: + trend, roll, symbols = list_strategy_snapshots_split(conn, limit=limit) + return { + "strategy_trend_records": trend, + "strategy_roll_records": roll, + "strategy_record_symbols": symbols, + "strategy_records_limit": limit, + "strategy_snapshots": trend + roll, + } def register_strategy_records(app, cfg: dict[str, Any]) -> None: diff --git a/strategy_snapshot_lib.py b/strategy_snapshot_lib.py index b2ebec3..e92b09a 100644 --- a/strategy_snapshot_lib.py +++ b/strategy_snapshot_lib.py @@ -7,6 +7,7 @@ from typing import Any, Callable, Optional STRATEGY_TREND = "trend_pullback" STRATEGY_ROLL = "roll" +STRATEGY_SNAPSHOTS_MAX_ROWS = 100 STRATEGY_SNAPSHOTS_SQL = """ CREATE TABLE IF NOT EXISTS strategy_trade_snapshots ( @@ -160,6 +161,7 @@ def save_trend_plan_snapshot( closed_at, ), ) + prune_strategy_snapshots(conn, keep=STRATEGY_SNAPSHOTS_MAX_ROWS) def save_roll_group_snapshot( @@ -220,6 +222,125 @@ def save_roll_group_snapshot( closed_at, ), ) + prune_strategy_snapshots(conn, keep=STRATEGY_SNAPSHOTS_MAX_ROWS) + + +def prune_strategy_snapshots(conn, *, keep: int = STRATEGY_SNAPSHOTS_MAX_ROWS) -> None: + """仅保留最近 keep 条策略快照(按 closed_at / id 倒序)。""" + k = max(1, min(int(keep), 500)) + conn.execute( + """DELETE FROM strategy_trade_snapshots + WHERE id NOT IN ( + SELECT id FROM strategy_trade_snapshots + ORDER BY COALESCE(closed_at, created_at, '') DESC, id DESC + LIMIT ? + )""", + (k,), + ) + + +def _snapshot_pnl(row: dict, snap: dict) -> float | None: + for key in ("pnl_amount",): + v = row.get(key) + if v is not None and v != "": + try: + return float(v) + except (TypeError, ValueError): + pass + v = snap.get("pnl_amount") + if v is not None and v != "": + try: + return float(v) + except (TypeError, ValueError): + pass + return None + + +def _trend_dca_stats(snap: dict) -> dict: + levels = snap.get("dca_levels") or build_trend_dca_levels(snap) + dca_only = [ + lv + for lv in levels + if (lv.get("leg_key") or "") != "first" and (lv.get("label") or "") != "首仓" + ] + done = sum(1 for lv in dca_only if lv.get("status") == "done") + total = len(dca_only) + pending = total - done + if total <= 0: + tag = "na" + elif done <= 0: + tag = "no_dca" + elif done >= total: + tag = "dca_done" + else: + tag = "dca_partial" + return { + "dca_done": done, + "dca_total": total, + "dca_pending": pending, + "dca_tag": tag, + } + + +def _roll_leg_stats(snap: dict) -> dict: + legs = snap.get("legs") or [] + if not isinstance(legs, list): + legs = [] + filled = sum(1 for lg in legs if (lg.get("status") or "").lower() == "filled") + total = len(legs) + pending = total - filled + if total <= 0: + tag = "na" + elif filled <= 0: + tag = "no_dca" + elif filled >= total: + tag = "dca_done" + else: + tag = "dca_partial" + return { + "dca_done": filled, + "dca_total": total, + "dca_pending": pending, + "dca_tag": tag, + } + + +def enrich_strategy_snapshot_row(row: dict) -> dict: + d = dict(row or {}) + snap = d.get("snapshot") or {} + st = (d.get("strategy_type") or "").strip() + pnl = _snapshot_pnl(d, snap) + if pnl is not None: + if pnl > 1e-9: + d["filter_pnl"] = "profit" + elif pnl < -1e-9: + d["filter_pnl"] = "loss" + else: + d["filter_pnl"] = "flat" + else: + d["filter_pnl"] = "unknown" + sym = (d.get("symbol") or d.get("exchange_symbol") or "").strip() + d["filter_symbol"] = sym.upper().split("/")[0].split(":")[0] if sym else "" + closed = (d.get("closed_at") or d.get("created_at") or "").strip() + d["sort_ts"] = closed + if st == STRATEGY_TREND: + stats = _trend_dca_stats(snap) + d.update(stats) + legs_txt = ( + f"{stats['dca_done']}/{stats['dca_total']}" + if stats["dca_total"] > 0 + else "0/0" + ) + d["summary_dca"] = legs_txt + else: + stats = _roll_leg_stats(snap) + d.update(stats) + d["summary_dca"] = ( + f"{stats['dca_done']}/{stats['dca_total']}腿" + if stats["dca_total"] > 0 + else "—" + ) + return d def list_strategy_snapshots(conn, *, limit: int = 200) -> list[dict]: @@ -237,5 +358,16 @@ def list_strategy_snapshots(conn, *, limit: int = 200) -> list[dict]: d["snapshot"] = {} st = (d.get("strategy_type") or "").strip() d["strategy_label"] = "趋势回调" if st == STRATEGY_TREND else "顺势加仓" - out.append(d) + out.append(enrich_strategy_snapshot_row(d)) return out + + +def list_strategy_snapshots_split( + conn, *, limit: int = STRATEGY_SNAPSHOTS_MAX_ROWS +) -> tuple[list[dict], list[dict], list[str]]: + """趋势 / 顺势分组,及筛选用币种列表。""" + all_rows = list_strategy_snapshots(conn, limit=limit) + trend = [r for r in all_rows if (r.get("strategy_type") or "") == STRATEGY_TREND] + roll = [r for r in all_rows if (r.get("strategy_type") or "") == STRATEGY_ROLL] + symbols = sorted({r.get("filter_symbol") or "" for r in all_rows if r.get("filter_symbol")}) + return trend, roll, symbols diff --git a/strategy_templates/strategy_records_page.html b/strategy_templates/strategy_records_page.html index fc78d3f..4b3a4e4 100644 --- a/strategy_templates/strategy_records_page.html +++ b/strategy_templates/strategy_records_page.html @@ -1,89 +1,276 @@ +{% set mf = money_fmt|default(funds_fmt) %}

策略交易记录

- 已结束的趋势回调计划与顺势加仓组会在此留存快照(含补仓档位明细)。保本移交、手动结束、止盈止损均会写入。 + 数据库保留最近 {{ strategy_records_limit|default(100) }} 条结束快照(按结束时间排序)。 + 趋势回调与顺势加仓分栏展示;点击行展开详情。结束计划、保本移交、止盈止损会自动写入。

- {% if strategy_snapshots %} -
- - - - - - - - - - - - {% for s in strategy_snapshots %} - {% set snap = s.snapshot or {} %} - {% set dca = snap.dca_levels if snap.dca_levels is defined else [] %} - {% set pnl = s.pnl_amount if s.pnl_amount is not none else snap.pnl_amount %} - - - - - - - - - - - {% endfor %} -
#策略品种方向结果盈亏U结束时间补仓明细
{{ s.id }}{{ s.strategy_label }}{{ s.symbol or s.exchange_symbol or '—' }}{{ '做多' if s.direction == 'long' else '做空' }}{{ s.result_label or '—' }} - {% if pnl is not none %}{{ funds_fmt(pnl) }}{% else %}—{% endif %} - {{ (s.closed_at or '')[:19] }} - {% if dca and dca|length %} -
- {{ dca|length }} 档 - + +
+ + +
+ 筛选 + + + + + +
+
+ +
+
+
+ 趋势回调记录 + {{ strategy_trend_records|length }} 条 +
+
+ {% for s in strategy_trend_records %} + {% set snap = s.snapshot or {} %} + {% set dca = snap.dca_levels if snap.dca_levels is defined else [] %} + {% set pnl = s.pnl_amount if s.pnl_amount is not none else snap.pnl_amount %} + {% set sym = s.symbol or s.exchange_symbol or '—' %} +
+
+ #{{ s.id }} {{ sym }} + {{ '做多' if s.direction == 'long' else '做空' }} + {{ s.result_label or '—' }} + {% if pnl is not none %}{{ mf(pnl) }}U{% else %}—{% endif %} + 补仓 {{ s.summary_dca or '—' }} + {{ (s.closed_at or '')[:16] }} +
+
+
+
计划 ID
{{ s.source_id or '—' }}
+
开仓
{{ (s.opened_at or '')[:16] or '—' }}
+
结束
{{ (s.closed_at or '')[:16] or '—' }}
+
均价
{% if snap.avg_entry_price is not none %}{{ price_fmt(sym, snap.avg_entry_price) }}{% else %}—{% endif %}
+
止损
{% if snap.stop_loss is not none %}{{ price_fmt(sym, snap.stop_loss) }}{% else %}—{% endif %}
+
止盈
{% if snap.take_profit is not none %}{{ price_fmt(sym, snap.take_profit) }}{% else %}—{% endif %}
+
风险%
{{ snap.risk_percent if snap.risk_percent is defined else '—' }}
+
杠杆
{{ snap.leverage if snap.leverage is defined else '—' }}x
+
计划保证金
{% if snap.plan_margin_capital is not none %}{{ mf(snap.plan_margin_capital) }}U{% else %}—{% endif %}
+
+ {% if dca and dca|length %} +
{% for lv in dca %} - + {% endfor %}
档位触发价张数状态
{{ lv.label or lv.leg_key }}{% if lv.price is not none %}{{ price_fmt(s.symbol or s.exchange_symbol, lv.price) }}{% else %}—{% endif %}{% if lv.price is not none %}{{ price_fmt(sym, lv.price) }}{% else %}—{% endif %} {% if lv.contracts is not none %}{{ lv.contracts }}{% else %}—{% endif %} {{ lv.status_label or '—' }}
-
- {% elif s.strategy_type == 'roll' and snap.legs %} -
- {{ snap.legs|length }} 腿 - - - {% for leg in snap.legs %} + {% endif %} + + + {% else %} +
暂无趋势回调结束记录
+ {% endfor %} + + + +
+
+ 顺势加仓记录 + {{ strategy_roll_records|length }} 条 +
+
+ {% for s in strategy_roll_records %} + {% set snap = s.snapshot or {} %} + {% set group = snap.group if snap.group is defined else {} %} + {% set legs = snap.legs if snap.legs is defined else [] %} + {% set pnl = s.pnl_amount if s.pnl_amount is not none else snap.pnl_amount %} + {% set sym = s.symbol or s.exchange_symbol or '—' %} +
+
+ #{{ s.id }} {{ sym }} + {{ '做多' if s.direction == 'long' else '做空' }} + {{ s.result_label or '—' }} + {% if pnl is not none %}{{ mf(pnl) }}U{% else %}—{% endif %} + 成交 {{ s.summary_dca or '—' }} + {{ (s.closed_at or '')[:16] }} +
+
+
+
组 ID
{{ s.source_id or '—' }}
+
创建
{{ (s.opened_at or group.created_at or '')[:16] or '—' }}
+
结束
{{ (s.closed_at or '')[:16] or '—' }}
+
状态
{{ s.status_at_close or group.status or '—' }}
+
杠杆
{{ group.leverage if group.leverage is defined else '—' }}x
+
备注
{{ group.message if group.message is defined else '—' }}
+
+ {% if legs and legs|length %} +
#状态
+ + {% for leg in legs %} - + + + {% endfor %}
腿次挂单价张数状态
{{ leg.leg_index or loop.index }}{{ leg.status_label or leg.status }}{% if leg.limit_price is not none %}{{ price_fmt(sym, leg.limit_price) }}{% else %}—{% endif %}{% if leg.order_amount is not none %}{{ leg.order_amount }}{% else %}—{% endif %}{{ leg.status_label or leg.status or '—' }}
-
- {% else %}—{% endif %} -
+ {% endif %} +
+
+ {% else %} +
暂无顺势加仓结束记录
+ {% endfor %} + + - {% else %} -
暂无策略结束快照。结束趋势回调计划或顺势加仓组后会自动出现在此。
- {% endif %} +