Files
CoinOX/lib/strategy/templates/strategy_records_page.html
dekun 88b7fb7a0a Initial CoinOX instance: coin-margined options + USDT hedge.
Forked from crypto_monitor OKX instance (copy-only). Keeps standalone options, perp-options and options-options hedge; drops standalone perp entrypoints; forces coin margin; adds sim wallets; default port 5010.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-14 00:18:40 +08:00

285 lines
17 KiB
HTML

{% set mf = money_fmt|default(funds_fmt) %}
<style>
.strategy-records-page{
padding:10px clamp(14px,2.2vw,22px) 22px;
box-sizing:border-box;
}
.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(280px,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;min-width:0}
.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;min-width:0}
.sr-item{border:1px solid #243050;border-radius:10px;background:#0f1424;overflow:visible}
.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 !important;line-height:1.45;min-height:2.4rem;min-width:0}
.sr-summary > span{flex:0 1 auto;min-width:0;color:inherit}
.sr-summary .sr-sym{color:#f0f2ff !important;flex-shrink:0}
.sr-summary .sr-dca-tag{color:#8892b0 !important}
.sr-summary .sr-pnl.pos{color:#4cd97f !important}
.sr-summary .sr-pnl.neg{color:#ff6666 !important}
.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>
<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 snap.symbol or snap.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(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>
{% 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 snap.symbol or snap.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>{% 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>
{% endif %}
</div>
</div>
{% else %}
<div class="sr-empty sr-empty-default">暂无顺势加仓结束记录</div>
{% endfor %}
</div>
</div>
</div>
</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>