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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
+133
-1
@@ -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
|
||||
|
||||
@@ -1,89 +1,276 @@
|
||||
{% set mf = money_fmt|default(funds_fmt) %}
|
||||
<style>
|
||||
.strategy-records-page{padding:4px 0 16px}
|
||||
.strategy-records-page h2{margin:0 0 10px;color:#dbe4ff}
|
||||
.strategy-records-tip{font-size:.78rem;color:#8892b0;line-height:1.55;margin-bottom:14px}
|
||||
.strategy-records-table{width:100%;border-collapse:collapse;font-size:.82rem}
|
||||
.strategy-records-table th,.strategy-records-table td{padding:8px 10px;border-bottom:1px solid #2a3150;text-align:left}
|
||||
.strategy-records-table th{color:#8b95b8;font-weight:600;font-size:.74rem}
|
||||
.strategy-records-table tr:hover td{background:rgba(42,63,108,.25)}
|
||||
.strategy-records-table .tag-trend{color:#6ab8ff}
|
||||
.strategy-records-table .tag-roll{color:#ffb020}
|
||||
.strategy-records-detail{margin-top:8px;padding:10px 12px;background:#0f1424;border:1px solid #2a3150;border-radius:8px;font-size:.76rem;color:#cfd3ef;line-height:1.5}
|
||||
.strategy-dca-mini{width:100%;margin-top:6px;border-collapse:collapse;font-size:.72rem}
|
||||
.strategy-dca-mini th,.strategy-dca-mini td{padding:4px 8px;border-bottom:1px solid #243050}
|
||||
.strategy-dca-mini .st-done{color:#4cd97f}
|
||||
.strategy-dca-mini .st-pending{color:#8892b0}
|
||||
.strategy-snap-pnl.pos{color:#4cd97f}
|
||||
.strategy-snap-pnl.neg{color:#ff6666}
|
||||
.strategy-records-page{padding:4px 0 20px}
|
||||
.strategy-records-page h2{margin:0 0 8px;color:#dbe4ff}
|
||||
.strategy-records-tip{font-size:.76rem;color:#8892b0;line-height:1.55;margin-bottom:12px}
|
||||
.sr-filters{display:flex;flex-wrap:wrap;gap:10px 14px;align-items:center;padding:12px 14px;background:#141a2a;border:1px solid #2a3150;border-radius:10px;margin-bottom:16px}
|
||||
.sr-filters label{font-size:.76rem;color:#8b95b8;display:flex;align-items:center;gap:6px}
|
||||
.sr-filters select,.sr-filters input[type=datetime-local]{padding:5px 8px;background:#0f1424;border:1px solid #304164;border-radius:6px;color:#dbe4ff;font-size:.78rem}
|
||||
.sr-chip-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
|
||||
.sr-chip{padding:5px 12px;border:1px solid #304164;border-radius:16px;background:#151a2a;color:#9aa3c4;font-size:.74rem;cursor:pointer;user-select:none}
|
||||
.sr-chip.active{background:#2a3f6c;color:#dbe4ff;border-color:#4a6a9a}
|
||||
.sr-panels{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
|
||||
@media (max-width:960px){.sr-panels{grid-template-columns:1fr}}
|
||||
.sr-panel{background:#141a2a;border:1px solid #2a3150;border-radius:12px;padding:12px 14px;min-height:120px}
|
||||
.sr-panel-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;gap:8px}
|
||||
.sr-panel-title{font-size:.92rem;font-weight:700;color:#f0f2ff}
|
||||
.sr-panel-title.trend{color:#6ab8ff}
|
||||
.sr-panel-title.roll{color:#ffb020}
|
||||
.sr-panel-count{font-size:.72rem;color:#8892b0}
|
||||
.sr-list{display:flex;flex-direction:column;gap:8px;max-height:62vh;overflow:auto}
|
||||
.sr-item{border:1px solid #243050;border-radius:10px;background:#0f1424;overflow:hidden}
|
||||
.sr-item.sr-hidden{display:none}
|
||||
.sr-summary{display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px;padding:10px 12px;cursor:pointer;font-size:.78rem;color:#cfd3ef;line-height:1.45}
|
||||
.sr-summary:hover{background:rgba(42,63,108,.2)}
|
||||
.sr-summary::before{content:"▸";color:#6ab8ff;margin-right:2px;transition:transform .15s}
|
||||
.sr-item.sr-open .sr-summary::before{transform:rotate(90deg)}
|
||||
.sr-summary .sr-sym{font-weight:600;color:#f0f2ff}
|
||||
.sr-summary .sr-pnl.pos{color:#4cd97f}
|
||||
.sr-summary .sr-pnl.neg{color:#ff6666}
|
||||
.sr-summary .sr-dca-tag{font-size:.7rem;color:#8892b0}
|
||||
.sr-detail{display:none;padding:0 12px 12px;border-top:1px dashed #2a3558;font-size:.76rem;color:#cfd3ef}
|
||||
.sr-item.sr-open .sr-detail{display:block}
|
||||
.sr-detail-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px 12px;margin:10px 0}
|
||||
.sr-detail-grid .lbl{color:#8b95b8;font-size:.7rem}
|
||||
.sr-detail-grid .val{color:#f0f2ff}
|
||||
.sr-dca-table{width:100%;border-collapse:collapse;font-size:.72rem;margin-top:8px}
|
||||
.sr-dca-table th,.sr-dca-table td{padding:5px 8px;border-bottom:1px solid #243050;text-align:left}
|
||||
.sr-dca-table th{color:#6a7598}
|
||||
.sr-dca-table .st-done{color:#4cd97f}
|
||||
.sr-dca-table .st-pending{color:#9aa3c4}
|
||||
.sr-empty{padding:20px;text-align:center;color:#8892b0;font-size:.8rem}
|
||||
</style>
|
||||
<div class="strategy-records-page card full">
|
||||
<h2>策略交易记录</h2>
|
||||
<p class="strategy-records-tip">
|
||||
已结束的趋势回调计划与顺势加仓组会在此留存快照(含补仓档位明细)。保本移交、手动结束、止盈止损均会写入。
|
||||
数据库保留最近 <strong>{{ strategy_records_limit|default(100) }}</strong> 条结束快照(按结束时间排序)。
|
||||
趋势回调与顺势加仓分栏展示;点击行展开详情。结束计划、保本移交、止盈止损会自动写入。
|
||||
</p>
|
||||
{% if strategy_snapshots %}
|
||||
<div class="table-wrap">
|
||||
<table class="strategy-records-table">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>策略</th>
|
||||
<th>品种</th>
|
||||
<th>方向</th>
|
||||
<th>结果</th>
|
||||
<th>盈亏U</th>
|
||||
<th>结束时间</th>
|
||||
<th>补仓明细</th>
|
||||
</tr>
|
||||
{% 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 %}
|
||||
<tr>
|
||||
<td>{{ s.id }}</td>
|
||||
<td><span class="{% if s.strategy_type == 'trend_pullback' %}tag-trend{% else %}tag-roll{% endif %}">{{ s.strategy_label }}</span></td>
|
||||
<td>{{ s.symbol or s.exchange_symbol or '—' }}</td>
|
||||
<td><span class="badge {{ 'direction-long' if s.direction == 'long' else 'direction-short' }}">{{ '做多' if s.direction == 'long' else '做空' }}</span></td>
|
||||
<td>{{ s.result_label or '—' }}</td>
|
||||
<td class="{% if pnl is not none %}{% if pnl|float > 0 %}strategy-snap-pnl pos{% elif pnl|float < 0 %}strategy-snap-pnl neg{% endif %}{% endif %}">
|
||||
{% if pnl is not none %}{{ funds_fmt(pnl) }}{% else %}—{% endif %}
|
||||
</td>
|
||||
<td>{{ (s.closed_at or '')[:19] }}</td>
|
||||
<td>
|
||||
{% if dca and dca|length %}
|
||||
<details>
|
||||
<summary style="cursor:pointer;color:#8fc8ff">{{ dca|length }} 档</summary>
|
||||
<table class="strategy-dca-mini">
|
||||
|
||||
<div class="sr-filters" id="sr-filters">
|
||||
<label>币种
|
||||
<select id="sr-filter-symbol">
|
||||
<option value="">全部</option>
|
||||
{% for sym in strategy_record_symbols %}
|
||||
<option value="{{ sym }}">{{ sym }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>时间
|
||||
<select id="sr-filter-time">
|
||||
<option value="desc">最新优先</option>
|
||||
<option value="asc">最早优先</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="sr-chip-row">
|
||||
<span style="font-size:.72rem;color:#8b95b8">筛选</span>
|
||||
<button type="button" class="sr-chip" data-filter="profit">盈利</button>
|
||||
<button type="button" class="sr-chip" data-filter="loss">亏损</button>
|
||||
<button type="button" class="sr-chip" data-filter="no_dca">未补仓</button>
|
||||
<button type="button" class="sr-chip" data-filter="dca">补仓</button>
|
||||
<button type="button" class="sr-chip" id="sr-filter-reset" style="border-style:dashed">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sr-panels">
|
||||
<div class="sr-panel" data-panel="trend">
|
||||
<div class="sr-panel-head">
|
||||
<span class="sr-panel-title trend">趋势回调记录</span>
|
||||
<span class="sr-panel-count" id="sr-trend-count">{{ strategy_trend_records|length }} 条</span>
|
||||
</div>
|
||||
<div class="sr-list" id="sr-trend-list">
|
||||
{% 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 '—' %}
|
||||
<div class="sr-item" data-symbol="{{ s.filter_symbol or '' }}" data-pnl="{{ s.filter_pnl or '' }}" data-dca-tag="{{ s.dca_tag or '' }}" data-dca-done="{{ s.dca_done|default(0) }}" data-sort-ts="{{ s.sort_ts or '' }}">
|
||||
<div class="sr-summary" role="button" tabindex="0">
|
||||
<span class="sr-sym">#{{ s.id }} {{ sym }}</span>
|
||||
<span class="badge {{ 'direction-long' if s.direction == 'long' else 'direction-short' }}">{{ '做多' if s.direction == 'long' else '做空' }}</span>
|
||||
<span>{{ s.result_label or '—' }}</span>
|
||||
<span class="sr-pnl {% if pnl is not none %}{% if pnl|float > 0 %}pos{% elif pnl|float < 0 %}neg{% endif %}{% endif %}">{% if pnl is not none %}{{ mf(pnl) }}U{% else %}—{% endif %}</span>
|
||||
<span class="sr-dca-tag">补仓 {{ s.summary_dca or '—' }}</span>
|
||||
<span style="color:#8892b0">{{ (s.closed_at or '')[:16] }}</span>
|
||||
</div>
|
||||
<div class="sr-detail">
|
||||
<div class="sr-detail-grid">
|
||||
<div><div class="lbl">计划 ID</div><div class="val">{{ s.source_id or '—' }}</div></div>
|
||||
<div><div class="lbl">开仓</div><div class="val">{{ (s.opened_at or '')[:16] or '—' }}</div></div>
|
||||
<div><div class="lbl">结束</div><div class="val">{{ (s.closed_at or '')[:16] or '—' }}</div></div>
|
||||
<div><div class="lbl">均价</div><div class="val">{% if snap.avg_entry_price is not none %}{{ price_fmt(sym, snap.avg_entry_price) }}{% else %}—{% endif %}</div></div>
|
||||
<div><div class="lbl">止损</div><div class="val">{% if snap.stop_loss is not none %}{{ price_fmt(sym, snap.stop_loss) }}{% else %}—{% endif %}</div></div>
|
||||
<div><div class="lbl">止盈</div><div class="val">{% if snap.take_profit is not none %}{{ price_fmt(sym, snap.take_profit) }}{% else %}—{% endif %}</div></div>
|
||||
<div><div class="lbl">风险%</div><div class="val">{{ snap.risk_percent if snap.risk_percent is defined else '—' }}</div></div>
|
||||
<div><div class="lbl">杠杆</div><div class="val">{{ snap.leverage if snap.leverage is defined else '—' }}x</div></div>
|
||||
<div><div class="lbl">计划保证金</div><div class="val">{% if snap.plan_margin_capital is not none %}{{ mf(snap.plan_margin_capital) }}U{% else %}—{% endif %}</div></div>
|
||||
</div>
|
||||
{% if dca and dca|length %}
|
||||
<table class="sr-dca-table">
|
||||
<tr><th>档位</th><th>触发价</th><th>张数</th><th>状态</th></tr>
|
||||
{% for lv in dca %}
|
||||
<tr>
|
||||
<td>{{ lv.label or lv.leg_key }}</td>
|
||||
<td>{% if lv.price is not none %}{{ price_fmt(s.symbol or s.exchange_symbol, lv.price) }}{% else %}—{% endif %}</td>
|
||||
<td>{% if lv.price is not none %}{{ price_fmt(sym, lv.price) }}{% else %}—{% endif %}</td>
|
||||
<td>{% if lv.contracts is not none %}{{ lv.contracts }}{% else %}—{% endif %}</td>
|
||||
<td class="{% if lv.status == 'done' %}st-done{% else %}st-pending{% endif %}">{{ lv.status_label or '—' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</details>
|
||||
{% elif s.strategy_type == 'roll' and snap.legs %}
|
||||
<details>
|
||||
<summary style="cursor:pointer;color:#8fc8ff">{{ snap.legs|length }} 腿</summary>
|
||||
<table class="strategy-dca-mini">
|
||||
<tr><th>#</th><th>状态</th></tr>
|
||||
{% for leg in snap.legs %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="sr-empty sr-empty-default">暂无趋势回调结束记录</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sr-panel" data-panel="roll">
|
||||
<div class="sr-panel-head">
|
||||
<span class="sr-panel-title roll">顺势加仓记录</span>
|
||||
<span class="sr-panel-count" id="sr-roll-count">{{ strategy_roll_records|length }} 条</span>
|
||||
</div>
|
||||
<div class="sr-list" id="sr-roll-list">
|
||||
{% 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 '—' %}
|
||||
<div class="sr-item" data-symbol="{{ s.filter_symbol or '' }}" data-pnl="{{ s.filter_pnl or '' }}" data-dca-tag="{{ s.dca_tag or '' }}" data-dca-done="{{ s.dca_done|default(0) }}" data-sort-ts="{{ s.sort_ts or '' }}">
|
||||
<div class="sr-summary" role="button" tabindex="0">
|
||||
<span class="sr-sym">#{{ s.id }} {{ sym }}</span>
|
||||
<span class="badge {{ 'direction-long' if s.direction == 'long' else 'direction-short' }}">{{ '做多' if s.direction == 'long' else '做空' }}</span>
|
||||
<span>{{ s.result_label or '—' }}</span>
|
||||
<span class="sr-pnl {% if pnl is not none %}{% if pnl|float > 0 %}pos{% elif pnl|float < 0 %}neg{% endif %}{% endif %}">{% if pnl is not none %}{{ mf(pnl) }}U{% else %}—{% endif %}</span>
|
||||
<span class="sr-dca-tag">成交 {{ s.summary_dca or '—' }}</span>
|
||||
<span style="color:#8892b0">{{ (s.closed_at or '')[:16] }}</span>
|
||||
</div>
|
||||
<div class="sr-detail">
|
||||
<div class="sr-detail-grid">
|
||||
<div><div class="lbl">组 ID</div><div class="val">{{ s.source_id or '—' }}</div></div>
|
||||
<div><div class="lbl">创建</div><div class="val">{{ (s.opened_at or group.created_at or '')[:16] or '—' }}</div></div>
|
||||
<div><div class="lbl">结束</div><div class="val">{{ (s.closed_at or '')[:16] or '—' }}</div></div>
|
||||
<div><div class="lbl">状态</div><div class="val">{{ s.status_at_close or group.status or '—' }}</div></div>
|
||||
<div><div class="lbl">杠杆</div><div class="val">{{ group.leverage if group.leverage is defined else '—' }}x</div></div>
|
||||
<div><div class="lbl">备注</div><div class="val">{{ group.message if group.message is defined else '—' }}</div></div>
|
||||
</div>
|
||||
{% if legs and legs|length %}
|
||||
<table class="sr-dca-table">
|
||||
<tr><th>腿次</th><th>挂单价</th><th>张数</th><th>状态</th></tr>
|
||||
{% for leg in legs %}
|
||||
<tr>
|
||||
<td>{{ leg.leg_index or loop.index }}</td>
|
||||
<td>{{ leg.status_label or leg.status }}</td>
|
||||
<td>{% if leg.limit_price is not none %}{{ price_fmt(sym, leg.limit_price) }}{% else %}—{% endif %}</td>
|
||||
<td>{% if leg.order_amount is not none %}{{ leg.order_amount }}{% else %}—{% endif %}</td>
|
||||
<td>{{ leg.status_label or leg.status or '—' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</details>
|
||||
{% else %}—{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="sr-empty sr-empty-default">暂无顺势加仓结束记录</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rule-tip" style="color:#8892b0">暂无策略结束快照。结束趋势回调计划或顺势加仓组后会自动出现在此。</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
const symbolSel = document.getElementById("sr-filter-symbol");
|
||||
const timeSel = document.getElementById("sr-filter-time");
|
||||
const chips = document.querySelectorAll(".sr-chip[data-filter]");
|
||||
const resetBtn = document.getElementById("sr-filter-reset");
|
||||
const active = new Set();
|
||||
|
||||
function itemMatches(el){
|
||||
const sym = (symbolSel && symbolSel.value) || "";
|
||||
if(sym && (el.getAttribute("data-symbol")||"").toUpperCase() !== sym.toUpperCase()) return false;
|
||||
if(active.has("profit") && el.getAttribute("data-pnl") !== "profit") return false;
|
||||
if(active.has("loss") && el.getAttribute("data-pnl") !== "loss") return false;
|
||||
if(active.has("no_dca")){
|
||||
const tag = el.getAttribute("data-dca-tag") || "";
|
||||
const done = parseInt(el.getAttribute("data-dca-done")||"0",10);
|
||||
if(tag !== "no_dca" && done > 0) return false;
|
||||
}
|
||||
if(active.has("dca")){
|
||||
const done = parseInt(el.getAttribute("data-dca-done")||"0",10);
|
||||
if(!(done > 0)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function sortList(listEl){
|
||||
if(!listEl) return;
|
||||
const asc = timeSel && timeSel.value === "asc";
|
||||
const items = Array.from(listEl.querySelectorAll(".sr-item"));
|
||||
items.sort((a,b)=>{
|
||||
const ta = a.getAttribute("data-sort-ts") || "";
|
||||
const tb = b.getAttribute("data-sort-ts") || "";
|
||||
if(ta === tb) return 0;
|
||||
return asc ? (ta < tb ? -1 : 1) : (ta > tb ? -1 : 1);
|
||||
});
|
||||
items.forEach(it => listEl.appendChild(it));
|
||||
}
|
||||
|
||||
function applyFilters(){
|
||||
["sr-trend-list","sr-roll-list"].forEach(id=>{
|
||||
const list = document.getElementById(id);
|
||||
if(!list) return;
|
||||
sortList(list);
|
||||
let visible = 0;
|
||||
list.querySelectorAll(".sr-item").forEach(el=>{
|
||||
const ok = itemMatches(el);
|
||||
el.classList.toggle("sr-hidden", !ok);
|
||||
if(ok) visible++;
|
||||
});
|
||||
const panel = list.closest(".sr-panel");
|
||||
const cnt = panel && panel.querySelector(".sr-panel-count");
|
||||
if(cnt) cnt.textContent = visible + " 条";
|
||||
let empty = list.querySelector(".sr-empty-filter");
|
||||
if(!visible && !list.querySelector(".sr-empty-default")){
|
||||
if(!empty){
|
||||
empty = document.createElement("div");
|
||||
empty.className = "sr-empty sr-empty-filter";
|
||||
empty.textContent = "无符合筛选的记录";
|
||||
list.appendChild(empty);
|
||||
}
|
||||
} else if(empty) empty.remove();
|
||||
});
|
||||
}
|
||||
|
||||
chips.forEach(ch=>{
|
||||
ch.addEventListener("click", ()=>{
|
||||
const k = ch.getAttribute("data-filter");
|
||||
if(active.has(k)) active.delete(k); else active.add(k);
|
||||
ch.classList.toggle("active", active.has(k));
|
||||
applyFilters();
|
||||
});
|
||||
});
|
||||
if(symbolSel) symbolSel.addEventListener("change", applyFilters);
|
||||
if(timeSel) timeSel.addEventListener("change", applyFilters);
|
||||
if(resetBtn) resetBtn.addEventListener("click", ()=>{
|
||||
active.clear();
|
||||
chips.forEach(c=>c.classList.remove("active"));
|
||||
if(symbolSel) symbolSel.value = "";
|
||||
if(timeSel) timeSel.value = "desc";
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
document.querySelectorAll(".sr-summary").forEach(sum=>{
|
||||
const toggle = ()=>{
|
||||
const item = sum.closest(".sr-item");
|
||||
if(item) item.classList.toggle("sr-open");
|
||||
};
|
||||
sum.addEventListener("click", toggle);
|
||||
sum.addEventListener("keydown", e=>{
|
||||
if(e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(); }
|
||||
});
|
||||
});
|
||||
|
||||
applyFilters();
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user