diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 56d5c95..961f7ed 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -6008,12 +6008,12 @@ def render_main_page(page="trade"): f"【阻力/支撑】填上/下沿,5m 收盘突破任一侧即提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分),不选方向、不自动开仓" ) strategy_extra = {} - if page in ("strategy", "strategy_trend", "strategy_roll"): - from strategy_ui import strategy_page_template_vars + if page in ("strategy", "strategy_trend", "strategy_roll", "strategy_records"): + from strategy_ui import strategy_render_extras - strategy_extra = strategy_page_template_vars( + strategy_extra = strategy_render_extras( conn, - "strategy", + page, default_risk_percent=float(RISK_PERCENT), request_obj=request, trend_cfg=app.extensions.get("strategy_trend_cfg"), diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index 26f6b41..e2dac27 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -259,6 +259,7 @@ 关键位监控 实盘下单 策略交易 + 策略交易记录 交易记录与复盘 统计分析 @@ -594,6 +595,8 @@ {% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %} {% include 'strategy_trading_page.html' %} + {% elif page == 'strategy_records' %} + {% include 'strategy_records_page.html' %} {% endif %} diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 0599e12..135b425 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -5964,12 +5964,12 @@ def render_main_page(page="trade"): f"【阻力/支撑】填上/下沿,5m 收盘突破任一侧即提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分),不选方向、不自动开仓" ) strategy_extra = {} - if page in ("strategy", "strategy_trend", "strategy_roll"): - from strategy_ui import strategy_page_template_vars + if page in ("strategy", "strategy_trend", "strategy_roll", "strategy_records"): + from strategy_ui import strategy_render_extras - strategy_extra = strategy_page_template_vars( + strategy_extra = strategy_render_extras( conn, - "strategy", + page, default_risk_percent=float(RISK_PERCENT), request_obj=request, trend_cfg=app.extensions.get("strategy_trend_cfg"), diff --git a/crypto_monitor_gate/templates/index.html b/crypto_monitor_gate/templates/index.html index 26f6b41..e2dac27 100644 --- a/crypto_monitor_gate/templates/index.html +++ b/crypto_monitor_gate/templates/index.html @@ -259,6 +259,7 @@ 关键位监控 实盘下单 策略交易 + 策略交易记录 交易记录与复盘 统计分析 @@ -594,6 +595,8 @@ {% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %} {% include 'strategy_trading_page.html' %} + {% elif page == 'strategy_records' %} + {% include 'strategy_records_page.html' %} {% endif %} diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 7e97f7c..382b44f 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -3824,7 +3824,9 @@ def enrich_active_trend_plan_row(row): d["floating_mark"] = float(m["mark_price"]) else: d["floating_mark"] = None - return d + from strategy_snapshot_lib import attach_trend_dca_levels + + return attach_trend_dca_levels(d) def opened_at_str_to_ms(opened_at_str): @@ -4602,6 +4604,25 @@ def _trend_finalize_plan(conn, row, result_label, exit_price, closed_at=None): if not getattr(cur, "rowcount", 0): return conn.commit() + try: + cfg = app.extensions.get("strategy_trend_cfg") or {} + closed = conn.execute( + "SELECT * FROM trend_pullback_plans WHERE id=?", (plan_id,) + ).fetchone() + if closed and cfg: + from strategy_snapshot_lib import save_trend_plan_snapshot + + save_trend_plan_snapshot( + cfg, + conn, + closed, + result_label=result_label, + exit_price=float(exit_price), + pnl_amount=float(pnl_amount) if pnl_amount is not None else None, + ) + conn.commit() + except Exception: + pass if _trend_plan_trade_exists(conn, plan_id): return session_date = row["session_date"] or get_trading_day() @@ -5457,7 +5478,11 @@ def render_main_page(page="trade"): elif pr: trend_preview_expired = True strategy_extra = {} - if page in ("strategy", "strategy_trend", "strategy_roll"): + if page == "strategy_records": + from strategy_ui import strategy_render_extras + + strategy_extra = strategy_render_extras(conn, page) + elif page in ("strategy", "strategy_trend", "strategy_roll"): from strategy_ui import fetch_roll_page_data strategy_extra = fetch_roll_page_data( diff --git a/crypto_monitor_gate_bot/templates/index.html b/crypto_monitor_gate_bot/templates/index.html index d43837a..f914392 100644 --- a/crypto_monitor_gate_bot/templates/index.html +++ b/crypto_monitor_gate_bot/templates/index.html @@ -248,6 +248,7 @@
交易执行 策略交易 + 策略交易记录 交易记录与复盘 统计分析
@@ -422,6 +423,8 @@ {% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %} {% set can_trade_trend = can_trade %} {% include 'strategy_trading_page.html' %} + {% elif page == 'strategy_records' %} + {% include 'strategy_records_page.html' %} {% endif %} {% if page == 'records' %} diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 3def956..282521a 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -5608,12 +5608,12 @@ def render_main_page(page="trade"): f"【阻力/支撑】填上/下沿,5m 收盘突破任一侧即提醒 {KEY_ALERT_MAX_TIMES} 次(间隔 {KEY_ALERT_INTERVAL_MINUTES} 分),不选方向、不自动开仓" ) strategy_extra = {} - if page in ("strategy", "strategy_trend", "strategy_roll"): - from strategy_ui import strategy_page_template_vars + if page in ("strategy", "strategy_trend", "strategy_roll", "strategy_records"): + from strategy_ui import strategy_render_extras - strategy_extra = strategy_page_template_vars( + strategy_extra = strategy_render_extras( conn, - "strategy", + page, default_risk_percent=float(RISK_PERCENT), request_obj=request, trend_cfg=app.extensions.get("strategy_trend_cfg"), diff --git a/crypto_monitor_okx/templates/index.html b/crypto_monitor_okx/templates/index.html index a818e88..a8b442b 100644 --- a/crypto_monitor_okx/templates/index.html +++ b/crypto_monitor_okx/templates/index.html @@ -259,6 +259,7 @@ 关键位监控 实盘下单 策略交易 + 策略交易记录 交易记录与复盘 统计分析 @@ -603,6 +604,8 @@ {% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %} {% include 'strategy_trading_page.html' %} + {% elif page == 'strategy_records' %} + {% include 'strategy_records_page.html' %} {% endif %} diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 87ddd6e..1fdcdf1 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -1375,15 +1375,84 @@ body.market-chart-fs-open { color: #8fc8ff; } -.exchange-fullscreen .hub-trend-plan-list { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 14px; - align-items: stretch; +.hub-trend-plan-card--horizontal { + display: flex; + flex-wrap: wrap; + gap: 14px 18px; + align-items: flex-start; + background: linear-gradient(145deg, rgba(12, 18, 32, 0.92), rgba(8, 12, 22, 0.88)); + border-color: rgba(0, 212, 255, 0.22); + box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35); } -.exchange-fullscreen .hub-trend-plan-card { - height: 100%; +.hub-trend-plan-body { + flex: 1 1 320px; + min-width: 0; +} + +.hub-trend-plan-side { + flex: 1 1 220px; + min-width: 200px; + max-width: 100%; +} + +.hub-trend-dca-block { + padding: 8px 10px; + background: rgba(0, 0, 0, 0.22); + border: 1px solid var(--border-soft); + border-radius: 8px; +} + +.hub-trend-dca-title { + font-size: 11px; + color: var(--muted); + margin-bottom: 6px; +} + +.hub-trend-dca-table { + width: 100%; + border-collapse: collapse; + font-size: 11px; +} + +.hub-trend-dca-table th, +.hub-trend-dca-table td { + padding: 4px 6px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + text-align: left; +} + +.hub-trend-dca-table th { + color: var(--muted); + font-weight: 600; +} + +.hub-trend-dca-table .dca-done { + color: var(--green); +} + +.hub-trend-dca-table .dca-pending { + color: var(--muted); +} + +.exchange-fullscreen .hub-trend-plan-list { + display: block; + max-width: 100%; +} + +.exchange-fullscreen .hub-trend-plan-card--horizontal { + width: 100%; +} + +.exchange-fullscreen .hub-trend-plan-metrics-row { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: 10px 16px; +} + +.exchange-fullscreen .hub-trend-plan-grid { + flex: 1 1 280px; } /* 顺势加仓 */ diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index a26339c..f95e033 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -1513,6 +1513,37 @@ return html; } + function renderTrendDcaTable(t, tickMap) { + const levels = Array.isArray(t.dca_levels) ? t.dca_levels : []; + if (!levels.length) return ""; + const sym = t.exchange_symbol || t.symbol || ""; + const rows = levels + .map((lv) => { + const price = + lv.price != null && lv.price !== "" + ? fmtSymbolPrice(lv.price, sym, tickMap) + : "—"; + const amt = + lv.contracts != null && lv.contracts !== "" ? esc(String(lv.contracts)) : "—"; + const stCls = lv.status === "done" ? "dca-done" : "dca-pending"; + const label = lv.status_label || (lv.status === "done" ? "已补仓" : "待补仓"); + return ` + ${esc(lv.label || lv.leg_key || "—")} + ${esc(price)} + ${amt} + ${esc(label)} + `; + }) + .join(""); + return `
+
补仓计划明细
+ + + ${rows} +
档位触发价张数状态
+
`; + } + function renderTrendPlanCard(t, tickMap, pos) { const sym = t.exchange_symbol || t.symbol || ""; const side = (t.direction || "long").toLowerCase(); @@ -1560,26 +1591,32 @@ 计划基数: ${baseTxt} 仓位占比: ${ratioTxt} `; - return `
-
- #${esc(t.id)} ${esc(sym)} ${renderDirectionHtml(t.direction)} - ${esc(t.status || "active")} + const dcaHtml = renderTrendDcaTable(t, tickMap); + return `
+
+
+ #${esc(t.id)} ${esc(sym)} ${renderDirectionHtml(t.direction)} + ${esc(t.status || "active")} +
+
+ 来源: 趋势回调计划 + 风险: ${riskTxt} + ${esc(trendAddZoneLabel(t.direction))} ${esc(addZone)} + 已补仓 ${legsTxt} +
+
+
+
均价${esc(avg)}
+
止损${esc(sl)}
+
止盈程序监控 · ${esc(tp)}
+
盈亏比${esc(rrTxt)}
+
标记价${esc(mark)}
+
浮盈亏${pnlInner}
+
+ ${footHtml} +
-
- 来源: 趋势回调计划 - 风险: ${riskTxt} - ${esc(trendAddZoneLabel(t.direction))} ${esc(addZone)} - 已补仓 ${legsTxt} -
-
-
均价${esc(avg)}
-
止损${esc(sl)}
-
止盈程序监控 · ${esc(tp)}
-
盈亏比${esc(rrTxt)}
-
标记价${esc(mark)}
-
浮盈亏${pnlInner}
-
- ${footHtml} + ${dcaHtml ? `
${dcaHtml}
` : ""}
`; } diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index f7fb823..5680c4c 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -14,7 +14,7 @@ - + @@ -235,6 +235,6 @@
- + diff --git a/strategy_db.py b/strategy_db.py index bcb9884..7acf013 100644 --- a/strategy_db.py +++ b/strategy_db.py @@ -133,11 +133,14 @@ CREATE TABLE IF NOT EXISTS trend_pullback_preview_snapshots ( def init_strategy_tables(conn) -> None: + from strategy_snapshot_lib import init_strategy_snapshot_table + conn.execute(ROLL_GROUPS_SQL) conn.execute(ROLL_LEGS_SQL) conn.execute(TREND_PLANS_SQL) conn.execute(TREND_PREVIEWS_SQL) conn.execute(TREND_PREVIEW_SNAPSHOTS_SQL) + init_strategy_snapshot_table(conn) for ddl in ( "ALTER TABLE trend_pullback_plans ADD COLUMN leg_amounts_json TEXT", "ALTER TABLE trend_pullback_plans ADD COLUMN initial_stop_loss REAL", diff --git a/strategy_records_register.py b/strategy_records_register.py new file mode 100644 index 0000000..67fbf03 --- /dev/null +++ b/strategy_records_register.py @@ -0,0 +1,55 @@ +"""策略交易记录页:已结束趋势 / 顺势加仓快照(四所统一)。""" +from __future__ import annotations + +import json +from typing import Any + +from flask import flash, redirect, url_for + +from strategy_snapshot_lib import list_strategy_snapshots + + +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 register_strategy_records(app, cfg: dict[str, Any]) -> None: + login_required = cfg["login_required"] + get_db = cfg["get_db"] + + def _lr(f): + return login_required(f) + + @_lr + @app.route("/strategy/records") + def strategy_records_page(): + m = cfg.get("app_module") + fn = getattr(m, "render_main_page", None) + if not callable(fn): + flash("render_main_page 未配置") + return redirect(url_for("strategy_trading_page")) + return fn("strategy_records") + + @_lr + @app.route("/strategy/records/") + def strategy_records_detail(snap_id: int): + conn = get_db() + row = conn.execute( + "SELECT * FROM strategy_trade_snapshots WHERE id=?", + (int(snap_id),), + ).fetchone() + conn.close() + if not row: + flash("未找到该策略快照") + return redirect(url_for("strategy_records_page")) + try: + snap = json.loads(row["snapshot_json"] or "{}") + except Exception: + snap = {} + dca = snap.get("dca_levels") or [] + flash( + f"快照 #{snap_id} {row['strategy_type']} {row['symbol']} " + f"{row['result_label']} · 补仓档 {len(dca)} 项(详情见列表页)" + ) + return redirect(url_for("strategy_records_page")) diff --git a/strategy_register.py b/strategy_register.py index dff416f..08689a6 100644 --- a/strategy_register.py +++ b/strategy_register.py @@ -20,6 +20,9 @@ def install_strategy_trading(app: Flask, repo_root: str, app_module: Any = None, attach_strategy_templates(app, repo_root) cfg = build_strategy_config(app_module, **build_kw) register_strategy_trading(app, cfg) + from strategy_records_register import register_strategy_records + + register_strategy_records(app, cfg) app.extensions["strategy_roll_cfg"] = cfg diff --git a/strategy_roll_monitor_lib.py b/strategy_roll_monitor_lib.py index 5000d00..905b813 100644 --- a/strategy_roll_monitor_lib.py +++ b/strategy_roll_monitor_lib.py @@ -76,6 +76,12 @@ def _close_roll_group( "UPDATE roll_groups SET status='closed', updated_at=? WHERE id=? AND status='active'", (_now(cfg), gid), ) + try: + from strategy_snapshot_lib import save_roll_group_snapshot + + save_roll_group_snapshot(cfg, conn, group, result_label="结束") + except Exception: + pass def _reconcile_roll_groups(conn, cfg: dict) -> None: diff --git a/strategy_snapshot_lib.py b/strategy_snapshot_lib.py new file mode 100644 index 0000000..b2ebec3 --- /dev/null +++ b/strategy_snapshot_lib.py @@ -0,0 +1,241 @@ +"""策略结束快照:趋势回调 / 顺势加仓(四所共用)。""" +from __future__ import annotations + +import json +from datetime import datetime, timezone +from typing import Any, Callable, Optional + +STRATEGY_TREND = "trend_pullback" +STRATEGY_ROLL = "roll" + +STRATEGY_SNAPSHOTS_SQL = """ +CREATE TABLE IF NOT EXISTS strategy_trade_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + strategy_type TEXT NOT NULL, + source_id INTEGER, + symbol TEXT, + exchange_symbol TEXT, + direction TEXT, + result_label TEXT, + status_at_close TEXT, + opened_at TEXT, + closed_at TEXT, + pnl_amount REAL, + snapshot_json TEXT NOT NULL, + created_at TEXT +) +""" + + +def init_strategy_snapshot_table(conn) -> None: + conn.execute(STRATEGY_SNAPSHOTS_SQL) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_strategy_snapshots_closed " + "ON strategy_trade_snapshots(closed_at DESC)" + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_strategy_snapshots_type " + "ON strategy_trade_snapshots(strategy_type, source_id)" + ) + + +def _row_dict(row) -> dict: + if row is None: + return {} + try: + return dict(row) + except Exception: + return {} + + +def _json_dumps(obj: Any) -> str: + return json.dumps(obj, ensure_ascii=False, separators=(",", ":")) + + +def build_trend_dca_levels(plan: dict) -> list[dict]: + """首仓 + 补仓档位列表(供策略页 / 中控)。""" + out: list[dict] = [] + p = plan or {} + try: + legs_done = int(p.get("legs_done") or 0) + except (TypeError, ValueError): + legs_done = 0 + try: + dca_legs = int(p.get("dca_legs") or 0) + except (TypeError, ValueError): + dca_legs = 0 + first_done = int(p.get("first_order_done") or 0) != 0 + try: + grid = json.loads(p.get("grid_prices_json") or "[]") + if not isinstance(grid, list): + grid = [] + except Exception: + grid = [] + try: + leg_amounts = json.loads(p.get("leg_amounts_json") or "[]") + if not isinstance(leg_amounts, list): + leg_amounts = [] + except Exception: + leg_amounts = [] + + out.append( + { + "i": 0, + "leg_key": "first", + "label": "首仓", + "price": None, + "contracts": p.get("first_order_amount"), + "status": "done" if first_done else "pending", + "status_label": "已开仓" if first_done else "待开仓", + } + ) + n = max(len(grid), len(leg_amounts), dca_legs) + for idx in range(n): + leg_i = idx + 1 + price = grid[idx] if idx < len(grid) else None + contracts = leg_amounts[idx] if idx < len(leg_amounts) else None + done = leg_i <= legs_done + out.append( + { + "i": leg_i, + "leg_key": f"dca_{leg_i}", + "label": f"补仓{leg_i}", + "price": price, + "contracts": contracts, + "status": "done" if done else "pending", + "status_label": "已补仓" if done else "待补仓", + } + ) + return out + + +def attach_trend_dca_levels(plan: dict) -> dict: + d = dict(plan or {}) + d["dca_levels"] = build_trend_dca_levels(d) + return d + + +def save_trend_plan_snapshot( + cfg: dict, + conn, + plan_row: Any, + *, + result_label: str, + exit_price: float | None = None, + pnl_amount: float | None = None, +) -> None: + init_strategy_snapshot_table(conn) + row = _row_dict(plan_row) + plan_id = int(row.get("id") or 0) + if plan_id <= 0: + return + m = cfg.get("app_module") + closed_at = ( + m.app_now_str() + if m is not None and hasattr(m, "app_now_str") + else datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + ) + payload = attach_trend_dca_levels(row) + payload["result_label"] = result_label + payload["exit_price"] = exit_price + payload["pnl_amount"] = pnl_amount + payload["status_at_close"] = row.get("status") + conn.execute( + """INSERT INTO strategy_trade_snapshots ( + strategy_type, source_id, symbol, exchange_symbol, direction, + result_label, status_at_close, opened_at, closed_at, pnl_amount, snapshot_json, created_at + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""", + ( + STRATEGY_TREND, + plan_id, + row.get("symbol"), + row.get("exchange_symbol"), + row.get("direction"), + result_label, + row.get("status"), + row.get("opened_at"), + closed_at, + pnl_amount, + _json_dumps(payload), + closed_at, + ), + ) + + +def save_roll_group_snapshot( + cfg: dict, + conn, + group: dict, + *, + result_label: str = "结束", + pnl_amount: float | None = None, +) -> None: + init_strategy_snapshot_table(conn) + g = dict(group or {}) + gid = int(g.get("id") or 0) + if gid <= 0: + return + legs = [] + for leg in conn.execute( + "SELECT * FROM roll_legs WHERE roll_group_id=? ORDER BY leg_index ASC, id ASC", + (gid,), + ).fetchall(): + ld = _row_dict(leg) + try: + from strategy_roll_monitor_lib import roll_leg_status_label + + ld["status_label"] = roll_leg_status_label(ld.get("status")) + except Exception: + ld["status_label"] = ld.get("status") or "" + legs.append(ld) + m = cfg.get("app_module") + closed_at = ( + m.app_now_str() + if m is not None and hasattr(m, "app_now_str") + else datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + ) + payload = { + "group": g, + "legs": legs, + "result_label": result_label, + "pnl_amount": pnl_amount, + } + conn.execute( + """INSERT INTO strategy_trade_snapshots ( + strategy_type, source_id, symbol, exchange_symbol, direction, + result_label, status_at_close, opened_at, closed_at, pnl_amount, snapshot_json, created_at + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""", + ( + STRATEGY_ROLL, + gid, + g.get("symbol"), + g.get("exchange_symbol"), + g.get("direction"), + result_label, + g.get("status"), + g.get("created_at"), + closed_at, + pnl_amount, + _json_dumps(payload), + closed_at, + ), + ) + + +def list_strategy_snapshots(conn, *, limit: int = 200) -> list[dict]: + init_strategy_snapshot_table(conn) + rows = conn.execute( + "SELECT * FROM strategy_trade_snapshots ORDER BY id DESC LIMIT ?", + (max(1, min(int(limit), 500)),), + ).fetchall() + out = [] + for r in rows: + d = _row_dict(r) + try: + d["snapshot"] = json.loads(d.get("snapshot_json") or "{}") + except Exception: + d["snapshot"] = {} + st = (d.get("strategy_type") or "").strip() + d["strategy_label"] = "趋势回调" if st == STRATEGY_TREND else "顺势加仓" + out.append(d) + return out diff --git a/strategy_templates/strategy_records_page.html b/strategy_templates/strategy_records_page.html new file mode 100644 index 0000000..fc78d3f --- /dev/null +++ b/strategy_templates/strategy_records_page.html @@ -0,0 +1,89 @@ + +
+

策略交易记录

+

+ 已结束的趋势回调计划与顺势加仓组会在此留存快照(含补仓档位明细)。保本移交、手动结束、止盈止损均会写入。 +

+ {% 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 }} 档 + + + {% 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.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 %} + + + + + {% endfor %} +
#状态
{{ leg.leg_index or loop.index }}{{ leg.status_label or leg.status }}
+
+ {% else %}—{% endif %} +
+
+ {% else %} +
暂无策略结束快照。结束趋势回调计划或顺势加仓组后会自动出现在此。
+ {% endif %} +
diff --git a/strategy_templates/strategy_trading_page.html b/strategy_templates/strategy_trading_page.html index eda9c40..91320d9 100644 --- a/strategy_templates/strategy_trading_page.html +++ b/strategy_templates/strategy_trading_page.html @@ -19,6 +19,13 @@ .plan-cell .val.pnl-neutral{color:#cfd3ef} .btn-close-plan{padding:7px 14px;background:#5c1e2a;color:#ffb4b4;border:none;border-radius:8px;cursor:pointer;font-size:.82rem;font-weight:600;text-decoration:none;white-space:nowrap;display:inline-block} .btn-close-plan:hover{filter:brightness(1.08)} +.plan-dca-block{margin-top:12px;padding-top:10px;border-top:1px dashed #2a3558} +.plan-dca-title{font-size:.74rem;color:#8b95b8;margin-bottom:8px;letter-spacing:.02em} +.plan-dca-table{width:100%;border-collapse:collapse;font-size:.76rem} +.plan-dca-table th,.plan-dca-table td{padding:6px 8px;border-bottom:1px solid #243050;text-align:left} +.plan-dca-table th{color:#6a7598;font-weight:600} +.plan-dca-table .st-done{color:#4cd97f} +.plan-dca-table .st-pending{color:#9aa3c4} @media (max-width:720px){ .plan-card-grid{grid-template-columns:1fr} } diff --git a/strategy_templates/strategy_trend_panel.html b/strategy_templates/strategy_trend_panel.html index 6a8d7c0..6a77b87 100644 --- a/strategy_templates/strategy_trend_panel.html +++ b/strategy_templates/strategy_trend_panel.html @@ -125,7 +125,7 @@ | {{ trend_add_zone_label(t.direction) }} {{ price_fmt(sym, t.add_upper) }} | 已补仓 {{ t.legs_done }}/{{ t.dca_legs }}
-
+
均价 {% if t.avg_entry_price is not none %}{{ price_fmt(sym, t.avg_entry_price) }}{% else %}—{% endif %} @@ -155,6 +155,22 @@
+ {% if t.dca_levels %} +
+
补仓计划明细
+ + + {% for lv in t.dca_levels %} + + + + + + + {% endfor %} +
档位触发价张数状态
{{ lv.label }}{% if lv.price is not none %}{{ price_fmt(sym, lv.price) }}{% else %}—{% endif %}{% if lv.contracts is not none %}{{ amt_disp(sym, lv.contracts) }}{% else %}—{% endif %}{{ lv.status_label }}
+
+ {% endif %}