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:
@@ -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