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 %} -| # | -策略 | -品种 | -方向 | -结果 | -盈亏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 }} 档-
- {{ snap.legs|length }} 腿-
暂无顺势加仓结束记录
+ {% endfor %}
+ 暂无策略结束快照。结束趋势回调计划或顺势加仓组后会自动出现在此。
- {% endif %}
+
|