Add tabbed period stats with lightweight charts on instance pages.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -166,6 +166,37 @@
|
|||||||
.list-window-bar label{color:#9aa;display:flex;align-items:center;gap:6px}
|
.list-window-bar label{color:#9aa;display:flex;align-items:center;gap:6px}
|
||||||
.stats-segment-block{margin-top:20px;padding-top:14px;border-top:1px solid #3a4468}
|
.stats-segment-block{margin-top:20px;padding-top:14px;border-top:1px solid #3a4468}
|
||||||
.stats-segment-block h2{font-size:1.05rem;color:#dbe4ff;margin-bottom:8px}
|
.stats-segment-block h2{font-size:1.05rem;color:#dbe4ff;margin-bottom:8px}
|
||||||
|
.stats-period-tabs{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}
|
||||||
|
.stats-period-tab{background:#151a2a;color:#9aa3bf;border:1px solid #304164;border-radius:8px;padding:7px 14px;font-size:.84rem;cursor:pointer;transition:background .15s,border-color .15s,color .15s}
|
||||||
|
.stats-period-tab:hover{background:#1c2438;color:#cfd3ef}
|
||||||
|
.stats-period-tab.active{background:#1f3a5a;color:#8fc8ff;border-color:#3d5f8a;font-weight:600}
|
||||||
|
.stats-period-pane[hidden]{display:none!important}
|
||||||
|
.stats-period-range{font-size:.78rem;color:#8892b0;margin-bottom:12px;line-height:1.45}
|
||||||
|
.inst-stats-viz{display:flex;flex-direction:column;gap:14px;margin-bottom:14px}
|
||||||
|
.inst-stats-kpis{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
|
||||||
|
.inst-stats-kpi{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;padding:12px 10px;background:#151a2a;border:1px solid #2a3152;border-radius:10px;text-align:center;min-height:88px}
|
||||||
|
.inst-stats-kpi-val{font-size:1.15rem;font-weight:700;font-variant-numeric:tabular-nums;line-height:1.2}
|
||||||
|
.inst-stats-kpi-lbl{font-size:.72rem;color:#8892b0;line-height:1.3}
|
||||||
|
.inst-stats-ring{--win-pct:0;width:56px;height:56px;border-radius:50%;background:conic-gradient(#4cd97f 0 calc(var(--win-pct) * 1%),#ff6b6b calc(var(--win-pct) * 1%) 100%);display:flex;align-items:center;justify-content:center;position:relative}
|
||||||
|
.inst-stats-ring::before{content:"";position:absolute;inset:7px;border-radius:50%;background:#151a2a}
|
||||||
|
.inst-stats-ring-label{position:relative;z-index:1;font-size:.78rem;font-weight:700;font-variant-numeric:tabular-nums}
|
||||||
|
.inst-stats-block{padding:12px;background:#141923;border:1px solid #2a3150;border-radius:10px}
|
||||||
|
.inst-stats-block-title{font-size:.72rem;color:#8892b0;margin-bottom:8px}
|
||||||
|
.inst-stats-stacked-bar{display:flex;height:10px;border-radius:6px;overflow:hidden;background:#1e2438}
|
||||||
|
.inst-stats-stacked-fill{height:100%;min-width:0;transition:width .2s ease}
|
||||||
|
.inst-stats-stacked-fill--profit{background:#4cd97f}
|
||||||
|
.inst-stats-stacked-fill--loss{background:#ff6b6b}
|
||||||
|
.inst-stats-bar-labels{display:flex;justify-content:space-between;gap:10px;margin-top:8px;font-size:.76rem;font-variant-numeric:tabular-nums}
|
||||||
|
.inst-stats-risk-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px}
|
||||||
|
.inst-stats-risk-item{display:flex;flex-direction:column;gap:3px;min-width:0}
|
||||||
|
.inst-stats-risk-item .k{font-size:.7rem;color:#8892b0}
|
||||||
|
.inst-stats-risk-item .v{font-size:.84rem;font-weight:600;font-variant-numeric:tabular-nums;color:#e8ecf4;word-break:break-word}
|
||||||
|
.inst-stats-empty{margin:0;padding:18px;text-align:center;color:#8892b0;font-size:.85rem;background:#141923;border:1px dashed #2a3348;border-radius:10px}
|
||||||
|
.inst-stats-details{margin-top:4px}
|
||||||
|
.inst-stats-details>summary{cursor:pointer;font-size:.84rem;color:#9aa3bf;padding:8px 0;user-select:none;list-style-position:inside}
|
||||||
|
.inst-stats-details>summary::-webkit-details-marker{color:#6d7689}
|
||||||
|
.inst-stats-details[open]>summary{margin-bottom:6px;color:#cfd3ef}
|
||||||
|
@media (max-width:640px){.inst-stats-kpis{grid-template-columns:1fr}.inst-stats-risk-grid{grid-template-columns:1fr}}
|
||||||
.key-history{margin-top:12px;padding-top:10px;border-top:1px solid #2a3150}
|
.key-history{margin-top:12px;padding-top:10px;border-top:1px solid #2a3150}
|
||||||
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
|
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
|
||||||
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
|
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
(function (global) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var PERIODS = ["day", "week", "month"];
|
||||||
|
|
||||||
|
function statsSegmentSelect() {
|
||||||
|
return document.getElementById("stats-segment-select");
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeSegmentPanel() {
|
||||||
|
var sel = statsSegmentSelect();
|
||||||
|
if (!sel) return null;
|
||||||
|
var key = sel.value;
|
||||||
|
return document.querySelector(
|
||||||
|
'.stats-segment-panel[data-stats-segment="' + key + '"]'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceStatsUrl(params) {
|
||||||
|
var q = new URLSearchParams(global.location.search);
|
||||||
|
Object.keys(params).forEach(function (k) {
|
||||||
|
if (params[k] == null || params[k] === "") q.delete(k);
|
||||||
|
else q.set(k, params[k]);
|
||||||
|
});
|
||||||
|
var qs = q.toString();
|
||||||
|
global.history.replaceState(
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
qs ? global.location.pathname + "?" + qs : global.location.pathname
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchStatsPeriod(periodKey) {
|
||||||
|
var panel = activeSegmentPanel();
|
||||||
|
if (!panel) return;
|
||||||
|
var key = PERIODS.indexOf(periodKey) >= 0 ? periodKey : "day";
|
||||||
|
panel.querySelectorAll(".stats-period-pane").forEach(function (pane) {
|
||||||
|
var match = pane.getAttribute("data-stats-period") === key;
|
||||||
|
if (match) pane.removeAttribute("hidden");
|
||||||
|
else pane.setAttribute("hidden", "");
|
||||||
|
});
|
||||||
|
panel.querySelectorAll(".stats-period-tab").forEach(function (btn) {
|
||||||
|
var on = btn.getAttribute("data-stats-period") === key;
|
||||||
|
btn.classList.toggle("active", on);
|
||||||
|
btn.setAttribute("aria-selected", on ? "true" : "false");
|
||||||
|
});
|
||||||
|
replaceStatsUrl({ stats_period: key });
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchStatsSegment() {
|
||||||
|
var sel = statsSegmentSelect();
|
||||||
|
if (!sel) return;
|
||||||
|
var key = sel.value;
|
||||||
|
document.querySelectorAll(".stats-segment-panel").forEach(function (p) {
|
||||||
|
p.style.display =
|
||||||
|
p.getAttribute("data-stats-segment") === key ? "block" : "none";
|
||||||
|
});
|
||||||
|
replaceStatsUrl({ stats_segment: key });
|
||||||
|
var period =
|
||||||
|
new URLSearchParams(global.location.search).get("stats_period") || "day";
|
||||||
|
switchStatsPeriod(period);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindPeriodTabs() {
|
||||||
|
document.querySelectorAll(".stats-period-tab").forEach(function (btn) {
|
||||||
|
if (btn.getAttribute("data-stats-bound") === "1") return;
|
||||||
|
btn.setAttribute("data-stats-bound", "1");
|
||||||
|
btn.addEventListener("click", function () {
|
||||||
|
switchStatsPeriod(btn.getAttribute("data-stats-period") || "day");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initStatsFromUrl() {
|
||||||
|
var sel = statsSegmentSelect();
|
||||||
|
if (!sel) return;
|
||||||
|
bindPeriodTabs();
|
||||||
|
var url = new URLSearchParams(global.location.search);
|
||||||
|
var segKey = url.get("stats_segment");
|
||||||
|
if (
|
||||||
|
segKey &&
|
||||||
|
sel.querySelector('option[value="' + segKey.replace(/"/g, "") + '"]')
|
||||||
|
) {
|
||||||
|
sel.value = segKey;
|
||||||
|
}
|
||||||
|
switchStatsSegment();
|
||||||
|
var period = url.get("stats_period") || "day";
|
||||||
|
if (PERIODS.indexOf(period) < 0) period = "day";
|
||||||
|
switchStatsPeriod(period);
|
||||||
|
}
|
||||||
|
|
||||||
|
global.switchStatsSegment = switchStatsSegment;
|
||||||
|
global.switchStatsPeriod = switchStatsPeriod;
|
||||||
|
global.initStatsFromUrl = initStatsFromUrl;
|
||||||
|
global.initStatsSegmentFromUrl = initStatsFromUrl;
|
||||||
|
})(window);
|
||||||
@@ -819,6 +819,46 @@ html[data-theme="light"] .stats-period-block {
|
|||||||
border-bottom-color: #d0dae4 !important;
|
border-bottom-color: #d0dae4 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-theme="light"] .stats-period-tab {
|
||||||
|
background: #f4f7fb !important;
|
||||||
|
color: #4a6078 !important;
|
||||||
|
border-color: #c8d4e0 !important;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .stats-period-tab:hover {
|
||||||
|
background: #e8eef5 !important;
|
||||||
|
color: #142232 !important;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .stats-period-tab.active {
|
||||||
|
background: #dce8f5 !important;
|
||||||
|
color: #0d4a7a !important;
|
||||||
|
border-color: #7eb0d8 !important;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .stats-period-range,
|
||||||
|
html[data-theme="light"] .inst-stats-kpi-lbl,
|
||||||
|
html[data-theme="light"] .inst-stats-block-title,
|
||||||
|
html[data-theme="light"] .inst-stats-risk-item .k,
|
||||||
|
html[data-theme="light"] .inst-stats-empty {
|
||||||
|
color: #4a6078 !important;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .inst-stats-kpi,
|
||||||
|
html[data-theme="light"] .inst-stats-block {
|
||||||
|
background: #f8fafc !important;
|
||||||
|
border-color: #c8d4e0 !important;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .inst-stats-ring::before {
|
||||||
|
background: #f8fafc !important;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .inst-stats-stacked-bar {
|
||||||
|
background: #e2e8f0 !important;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .inst-stats-risk-item .v,
|
||||||
|
html[data-theme="light"] .inst-stats-details > summary {
|
||||||
|
color: #142232 !important;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .inst-stats-details[open] > summary {
|
||||||
|
color: #0d4a7a !important;
|
||||||
|
}
|
||||||
|
|
||||||
html[data-theme="light"] .key-history {
|
html[data-theme="light"] .key-history {
|
||||||
border-top-color: #d0dae4 !important;
|
border-top-color: #d0dae4 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -597,29 +597,6 @@ function recomputeJournalRealRr(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function switchStatsSegment(){
|
|
||||||
const sel = document.getElementById("stats-segment-select");
|
|
||||||
if(!sel) return;
|
|
||||||
const key = sel.value;
|
|
||||||
document.querySelectorAll(".stats-segment-panel").forEach(p=>{
|
|
||||||
p.style.display = p.getAttribute("data-stats-segment") === key ? "block" : "none";
|
|
||||||
});
|
|
||||||
const q = new URLSearchParams(window.location.search);
|
|
||||||
q.set("stats_segment", key);
|
|
||||||
const qs = q.toString();
|
|
||||||
history.replaceState(null, "", qs ? (window.location.pathname + "?" + qs) : window.location.pathname);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initStatsSegmentFromUrl(){
|
|
||||||
const sel = document.getElementById("stats-segment-select");
|
|
||||||
if(!sel) return;
|
|
||||||
const key = new URLSearchParams(window.location.search).get("stats_segment");
|
|
||||||
if(key && sel.querySelector('option[value="' + key.replace(/"/g, "") + '"]')){
|
|
||||||
sel.value = key;
|
|
||||||
}
|
|
||||||
switchStatsSegment();
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleStatsCard(){
|
function toggleStatsCard(){
|
||||||
const card = document.getElementById("stats-card");
|
const card = document.getElementById("stats-card");
|
||||||
const btn = document.getElementById("stats-toggle-btn");
|
const btn = document.getElementById("stats-toggle-btn");
|
||||||
|
|||||||
@@ -1,24 +1,99 @@
|
|||||||
{# Hub iframe tab fragment — shared via embed_templates #}
|
{# Hub iframe tab fragment — shared via embed_templates #}
|
||||||
{% macro period_stats(title, s) %}
|
{% macro period_stats_pane(period_key, s) %}
|
||||||
<div class="stats-period-block">
|
{% set win_pct = s.win_rate_pct if s.win_rate_pct is not none else 0 %}
|
||||||
<h3>{{ title }}</h3>
|
{% set profit_sum = (s.net_pnl_u + s.loss_sum_u) if s.closed_count else 0 %}
|
||||||
<div class="sub">{{ s.range_label }}</div>
|
{% set loss_sum = s.loss_sum_u %}
|
||||||
<div class="stats-detail">
|
{% set pnl_total = profit_sum + loss_sum %}
|
||||||
<div class="stat-item"><div class="label">开单次数</div><div class="value">{{ s.opens_count }}</div></div>
|
{% set profit_bar_w = (profit_sum / pnl_total * 100) if pnl_total > 0 else 0 %}
|
||||||
<div class="stat-item"><div class="label">平仓笔数</div><div class="value">{{ s.closed_count }}</div></div>
|
{% set loss_bar_w = (loss_sum / pnl_total * 100) if pnl_total > 0 else 0 %}
|
||||||
<div class="stat-item"><div class="label">胜率</div><div class="value">{% if s.win_rate_pct is not none %}{{ s.win_rate_pct }}%{% else %}-{% endif %}</div></div>
|
{% set net_cls = 'pos-pnl-profit' if s.net_pnl_u > 0 else ('pos-pnl-loss' if s.net_pnl_u < 0 else '') %}
|
||||||
<div class="stat-item"><div class="label">净盈亏(U)</div><div class="value">{{ funds_fmt(s.net_pnl_u) }}</div></div>
|
<div class="stats-period-pane" data-stats-period="{{ period_key }}" role="tabpanel"{% if period_key != 'day' %} hidden{% endif %}>
|
||||||
<div class="stat-item"><div class="label">亏损额合计(U)</div><div class="value">{{ funds_fmt(s.loss_sum_u) }}</div></div>
|
<div class="stats-period-range">{{ s.range_label }}</div>
|
||||||
<div class="stat-item"><div class="label">单笔最大亏损(U)</div><div class="value">{% if s.max_single_loss is not none %}{{ funds_fmt(s.max_single_loss) }}{% else %}-{% endif %}</div></div>
|
<div class="inst-stats-viz">
|
||||||
<div class="stat-item"><div class="label">单笔最大盈利(U)</div><div class="value">{% if s.max_single_profit is not none %}{{ funds_fmt(s.max_single_profit) }}{% else %}-{% endif %}</div></div>
|
{% if s.closed_count %}
|
||||||
<div class="stat-item"><div class="label">最大回撤(U)</div><div class="value">{{ funds_fmt(s.max_drawdown_u) }}</div></div>
|
<div class="inst-stats-kpis">
|
||||||
<div class="stat-item"><div class="label">当前连续亏损笔数</div><div class="value">{{ s.consecutive_losses }}</div></div>
|
<div class="inst-stats-kpi inst-stats-kpi--pnl">
|
||||||
<div class="stat-item"><div class="label">最长连续亏损(交易日)</div><div class="value">{{ s.max_loss_streak_days }} 天</div></div>
|
<span class="inst-stats-kpi-val {{ net_cls }}">{% if s.net_pnl_u > 0 %}+{% endif %}{{ funds_fmt(s.net_pnl_u) }}U</span>
|
||||||
<div class="stat-item"><div class="label">期内最大亏损日</div><div class="value">{% if s.worst_day %}{{ s.worst_day }}({{ funds_fmt(s.worst_day_pnl) }}U){% else %}-{% endif %}</div></div>
|
<span class="inst-stats-kpi-lbl">净盈亏</span>
|
||||||
|
</div>
|
||||||
|
<div class="inst-stats-kpi inst-stats-kpi--win">
|
||||||
|
<div class="inst-stats-ring" style="--win-pct: {{ win_pct }}">
|
||||||
|
<span class="inst-stats-ring-label">{% if s.win_rate_pct is not none %}{{ win_pct|round(0)|int }}%{% else %}—{% endif %}</span>
|
||||||
|
</div>
|
||||||
|
<span class="inst-stats-kpi-lbl">{{ s.win_count }}胜 {{ s.loss_count }}负</span>
|
||||||
|
</div>
|
||||||
|
<div class="inst-stats-kpi inst-stats-kpi--trades">
|
||||||
|
<span class="inst-stats-kpi-val">{{ s.opens_count }} / {{ s.closed_count }}</span>
|
||||||
|
<span class="inst-stats-kpi-lbl">开单 / 平仓</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inst-stats-block">
|
||||||
|
<div class="inst-stats-block-title">盈亏构成</div>
|
||||||
|
<div class="inst-stats-stacked-bar">
|
||||||
|
<div class="inst-stats-stacked-fill inst-stats-stacked-fill--profit" style="width: {{ '%.1f'|format(profit_bar_w) }}%"></div>
|
||||||
|
<div class="inst-stats-stacked-fill inst-stats-stacked-fill--loss" style="width: {{ '%.1f'|format(loss_bar_w) }}%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="inst-stats-bar-labels">
|
||||||
|
<span class="pos-pnl-profit">盈利 {{ funds_fmt(profit_sum) }}U</span>
|
||||||
|
<span class="pos-pnl-loss">亏损 {{ funds_fmt(loss_sum) }}U</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inst-stats-block inst-stats-block--risk">
|
||||||
|
<div class="inst-stats-risk-grid">
|
||||||
|
<div class="inst-stats-risk-item">
|
||||||
|
<span class="k">最大回撤</span>
|
||||||
|
<span class="v pos-pnl-loss">{{ funds_fmt(s.max_drawdown_u) }}U</span>
|
||||||
|
</div>
|
||||||
|
<div class="inst-stats-risk-item">
|
||||||
|
<span class="k">连续亏损</span>
|
||||||
|
<span class="v">{{ s.consecutive_losses }} 笔</span>
|
||||||
|
</div>
|
||||||
|
<div class="inst-stats-risk-item">
|
||||||
|
<span class="k">最长连亏日</span>
|
||||||
|
<span class="v">{{ s.max_loss_streak_days }} 天</span>
|
||||||
|
</div>
|
||||||
|
<div class="inst-stats-risk-item">
|
||||||
|
<span class="k">最大亏损日</span>
|
||||||
|
<span class="v">{% if s.worst_day %}{{ s.worst_day }} ({{ funds_fmt(s.worst_day_pnl) }}U){% else %}—{% endif %}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="inst-stats-empty">当前区间暂无平仓数据</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<details class="inst-stats-details" open>
|
||||||
|
<summary>详细指标</summary>
|
||||||
|
<div class="stats-detail">
|
||||||
|
<div class="stat-item"><div class="label">开单次数</div><div class="value">{{ s.opens_count }}</div></div>
|
||||||
|
<div class="stat-item"><div class="label">平仓笔数</div><div class="value">{{ s.closed_count }}</div></div>
|
||||||
|
<div class="stat-item"><div class="label">胜率</div><div class="value">{% if s.win_rate_pct is not none %}{{ s.win_rate_pct }}%{% else %}-{% endif %}</div></div>
|
||||||
|
<div class="stat-item"><div class="label">净盈亏(U)</div><div class="value">{{ funds_fmt(s.net_pnl_u) }}</div></div>
|
||||||
|
<div class="stat-item"><div class="label">亏损额合计(U)</div><div class="value">{{ funds_fmt(s.loss_sum_u) }}</div></div>
|
||||||
|
<div class="stat-item"><div class="label">单笔最大亏损(U)</div><div class="value">{% if s.max_single_loss is not none %}{{ funds_fmt(s.max_single_loss) }}{% else %}-{% endif %}</div></div>
|
||||||
|
<div class="stat-item"><div class="label">单笔最大盈利(U)</div><div class="value">{% if s.max_single_profit is not none %}{{ funds_fmt(s.max_single_profit) }}{% else %}-{% endif %}</div></div>
|
||||||
|
<div class="stat-item"><div class="label">最大回撤(U)</div><div class="value">{{ funds_fmt(s.max_drawdown_u) }}</div></div>
|
||||||
|
<div class="stat-item"><div class="label">当前连续亏损笔数</div><div class="value">{{ s.consecutive_losses }}</div></div>
|
||||||
|
<div class="stat-item"><div class="label">最长连续亏损(交易日)</div><div class="value">{{ s.max_loss_streak_days }} 天</div></div>
|
||||||
|
<div class="stat-item"><div class="label">期内最大亏损日</div><div class="value">{% if s.worst_day %}{{ s.worst_day }}({{ funds_fmt(s.worst_day_pnl) }}U){% else %}-{% endif %}</div></div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
|
</div>
|
||||||
|
<a href="/env_config" class="{% if page == 'env_config' %}active{% endif %}">env配置</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="/settings" class="{% if page == 'settings' %}active{% endif %}">系统设置</a>
|
||||||
|
</div>
|
||||||
|
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
|
||||||
|
|
||||||
|
{% include 'instance_header_panel.html' %}
|
||||||
|
{% if page not in ('settings', 'risk_policy', 'env_config', 'options') %}
|
||||||
|
{% include 'instance_top_bar.html' %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
{% if page == 'key_monitor' %}
|
{% if page == 'key_monitor' %}
|
||||||
{% include 'key_monitor_panel.html' %}
|
{% include 'key_monitor_panel.html' %}
|
||||||
{% elif page == 'trade' %}
|
{% elif page == 'trade' %}
|
||||||
@@ -80,6 +155,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 style="margin-bottom:8px">实时持仓</h2>
|
<h2 style="margin-bottom:8px">实时持仓</h2>
|
||||||
|
{% if ui_orphan_recovery_enabled %}
|
||||||
|
{% if not order and orphan_live_positions %}
|
||||||
|
{% set o = orphan_live_positions[0] %}
|
||||||
|
<div id="orphan-position-recover" class="orphan-recover-banner" style="display:block;margin-bottom:10px;padding:10px 12px;background:#2a2210;border:1px solid #6b5420;border-radius:6px;font-size:.9rem;color:#e8d5a8">
|
||||||
|
检测到交易所仍有 <strong>{{ o.symbol }}</strong> {{ '空' if o.direction == 'short' else '多' }}仓,但本地监控已中断(误同步时可能无交易记录).
|
||||||
|
{% if o.recoverable_monitor_id %}
|
||||||
|
<button type="button" class="pos-entrust-btn" onclick="recoverLivePosition({{ o.recoverable_monitor_id }})">恢复监控{% if o.plan_stop_loss and o.plan_take_profit %}并挂止盈止损{% endif %}</button>
|
||||||
|
{% else %}
|
||||||
|
未找到可恢复的监控记录,需在服务器数据库处理.
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div id="orphan-position-recover" class="orphan-recover-banner" style="display:none;margin-bottom:10px;padding:10px 12px;background:#2a2210;border:1px solid #6b5420;border-radius:6px;font-size:.9rem;color:#e8d5a8"></div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
<div class="panel-scroll pos-list pos-list-live">
|
<div class="panel-scroll pos-list pos-list-live">
|
||||||
{% for o in order %}
|
{% for o in order %}
|
||||||
<div class="pos-card" id="order-row-{{ o.id }}"
|
<div class="pos-card" id="order-row-{{ o.id }}"
|
||||||
@@ -399,16 +489,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if page == 'env_config' %}
|
{% if page == 'env_config' %}
|
||||||
{% include 'env_config_panel.html' %}
|
{% include 'env_config_panel.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if page == 'risk_policy' %}
|
{% if page == 'risk_policy' %}
|
||||||
{% include 'risk_policy_panel.html' %}
|
{% include 'risk_policy_panel.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if page == 'settings' %}
|
{% if page == 'settings' %}
|
||||||
{% include 'settings_panel.html' %}
|
{% include 'settings_panel.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if page == 'stats' %}
|
{% if page == 'stats' %}
|
||||||
<div class="card stats-card full" id="stats-card">
|
<div class="card stats-card full" id="stats-card">
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap">
|
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap">
|
||||||
@@ -432,9 +526,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% for seg in stats_bundle.segments %}
|
{% for seg in stats_bundle.segments %}
|
||||||
<div class="stats-segment-block stats-segment-panel" data-stats-segment="{{ seg.key }}"{% if not loop.first %} style="display:none"{% endif %}>
|
<div class="stats-segment-block stats-segment-panel" data-stats-segment="{{ seg.key }}"{% if not loop.first %} style="display:none"{% endif %}>
|
||||||
{{ period_stats("日统计", seg.day) }}
|
<div class="stats-period-tabs" role="tablist" aria-label="统计周期">
|
||||||
{{ period_stats("周统计", seg.week) }}
|
<button type="button" class="stats-period-tab active" data-stats-period="day" role="tab" aria-selected="true">日统计</button>
|
||||||
{{ period_stats("月统计", seg.month) }}
|
<button type="button" class="stats-period-tab" data-stats-period="week" role="tab" aria-selected="false">周统计</button>
|
||||||
|
<button type="button" class="stats-period-tab" data-stats-period="month" role="tab" aria-selected="false">月统计</button>
|
||||||
|
</div>
|
||||||
|
{{ period_stats_pane("day", seg.day) }}
|
||||||
|
{{ period_stats_pane("week", seg.week) }}
|
||||||
|
{{ period_stats_pane("month", seg.month) }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
<script src="/static/instance_theme.js?v=50"></script>
|
<script src="/static/instance_theme.js?v=50"></script>
|
||||||
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
|
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
|
||||||
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
|
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
|
||||||
<link rel="stylesheet" href="/static/instance_page.css?v=4">
|
<link rel="stylesheet" href="/static/instance_page.css?v=5">
|
||||||
<link rel="stylesheet" href="/static/instance_theme.css?v=77">
|
<link rel="stylesheet" href="/static/instance_theme.css?v=78">
|
||||||
<script src="/static/account_risk_badge.js?v=4"></script>
|
<script src="/static/account_risk_badge.js?v=4"></script>
|
||||||
<meta name="theme-color" content="#0b0d14">
|
<meta name="theme-color" content="#0b0d14">
|
||||||
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
|
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
|
||||||
@@ -100,6 +100,7 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj
|
|||||||
<script src="/static/symbol_live_price.js?v=2"></script>
|
<script src="/static/symbol_live_price.js?v=2"></script>
|
||||||
<script src="/static/strategy_roll.js?v=6"></script>
|
<script src="/static/strategy_roll.js?v=6"></script>
|
||||||
<script src="/static/key_monitor_form.js?v=2"></script>
|
<script src="/static/key_monitor_form.js?v=2"></script>
|
||||||
|
<script src="/static/instance_stats.js?v=1"></script>
|
||||||
{% include 'embed_boot_scripts.html' %}
|
{% include 'embed_boot_scripts.html' %}
|
||||||
<script>
|
<script>
|
||||||
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
|
||||||
<link rel="manifest" href="/static/icons/manifest.webmanifest">
|
<link rel="manifest" href="/static/icons/manifest.webmanifest">
|
||||||
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
|
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
|
||||||
<link rel="stylesheet" href="/static/instance_page.css?v=1">
|
<link rel="stylesheet" href="/static/instance_page.css?v=2">
|
||||||
<link rel="stylesheet" href="/static/instance_theme.css?v=77">
|
<link rel="stylesheet" href="/static/instance_theme.css?v=78">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
@@ -30,23 +30,85 @@
|
|||||||
data-full-margin-buffer="{{ full_margin_buffer_ratio }}"
|
data-full-margin-buffer="{{ full_margin_buffer_ratio }}"
|
||||||
data-price-refresh-ms="{{ price_refresh_seconds * 1000 }}"
|
data-price-refresh-ms="{{ price_refresh_seconds * 1000 }}"
|
||||||
>
|
>
|
||||||
{% macro period_stats(title, s) %}
|
{% macro period_stats_pane(period_key, s) %}
|
||||||
<div class="stats-period-block">
|
{% set win_pct = s.win_rate_pct if s.win_rate_pct is not none else 0 %}
|
||||||
<h3>{{ title }}</h3>
|
{% set profit_sum = (s.net_pnl_u + s.loss_sum_u) if s.closed_count else 0 %}
|
||||||
<div class="sub">{{ s.range_label }}</div>
|
{% set loss_sum = s.loss_sum_u %}
|
||||||
<div class="stats-detail">
|
{% set pnl_total = profit_sum + loss_sum %}
|
||||||
<div class="stat-item"><div class="label">开单次数</div><div class="value">{{ s.opens_count }}</div></div>
|
{% set profit_bar_w = (profit_sum / pnl_total * 100) if pnl_total > 0 else 0 %}
|
||||||
<div class="stat-item"><div class="label">平仓笔数</div><div class="value">{{ s.closed_count }}</div></div>
|
{% set loss_bar_w = (loss_sum / pnl_total * 100) if pnl_total > 0 else 0 %}
|
||||||
<div class="stat-item"><div class="label">胜率</div><div class="value">{% if s.win_rate_pct is not none %}{{ s.win_rate_pct }}%{% else %}-{% endif %}</div></div>
|
{% set net_cls = 'pos-pnl-profit' if s.net_pnl_u > 0 else ('pos-pnl-loss' if s.net_pnl_u < 0 else '') %}
|
||||||
<div class="stat-item"><div class="label">净盈亏(U)</div><div class="value">{{ funds_fmt(s.net_pnl_u) }}</div></div>
|
<div class="stats-period-pane" data-stats-period="{{ period_key }}" role="tabpanel"{% if period_key != 'day' %} hidden{% endif %}>
|
||||||
<div class="stat-item"><div class="label">亏损额合计(U)</div><div class="value">{{ funds_fmt(s.loss_sum_u) }}</div></div>
|
<div class="stats-period-range">{{ s.range_label }}</div>
|
||||||
<div class="stat-item"><div class="label">单笔最大亏损(U)</div><div class="value">{% if s.max_single_loss is not none %}{{ funds_fmt(s.max_single_loss) }}{% else %}-{% endif %}</div></div>
|
<div class="inst-stats-viz">
|
||||||
<div class="stat-item"><div class="label">单笔最大盈利(U)</div><div class="value">{% if s.max_single_profit is not none %}{{ funds_fmt(s.max_single_profit) }}{% else %}-{% endif %}</div></div>
|
{% if s.closed_count %}
|
||||||
<div class="stat-item"><div class="label">最大回撤(U)</div><div class="value">{{ funds_fmt(s.max_drawdown_u) }}</div></div>
|
<div class="inst-stats-kpis">
|
||||||
<div class="stat-item"><div class="label">当前连续亏损笔数</div><div class="value">{{ s.consecutive_losses }}</div></div>
|
<div class="inst-stats-kpi inst-stats-kpi--pnl">
|
||||||
<div class="stat-item"><div class="label">最长连续亏损(交易日)</div><div class="value">{{ s.max_loss_streak_days }} 天</div></div>
|
<span class="inst-stats-kpi-val {{ net_cls }}">{% if s.net_pnl_u > 0 %}+{% endif %}{{ funds_fmt(s.net_pnl_u) }}U</span>
|
||||||
<div class="stat-item"><div class="label">期内最大亏损日</div><div class="value">{% if s.worst_day %}{{ s.worst_day }}({{ funds_fmt(s.worst_day_pnl) }}U){% else %}-{% endif %}</div></div>
|
<span class="inst-stats-kpi-lbl">净盈亏</span>
|
||||||
|
</div>
|
||||||
|
<div class="inst-stats-kpi inst-stats-kpi--win">
|
||||||
|
<div class="inst-stats-ring" style="--win-pct: {{ win_pct }}">
|
||||||
|
<span class="inst-stats-ring-label">{% if s.win_rate_pct is not none %}{{ win_pct|round(0)|int }}%{% else %}—{% endif %}</span>
|
||||||
|
</div>
|
||||||
|
<span class="inst-stats-kpi-lbl">{{ s.win_count }}胜 {{ s.loss_count }}负</span>
|
||||||
|
</div>
|
||||||
|
<div class="inst-stats-kpi inst-stats-kpi--trades">
|
||||||
|
<span class="inst-stats-kpi-val">{{ s.opens_count }} / {{ s.closed_count }}</span>
|
||||||
|
<span class="inst-stats-kpi-lbl">开单 / 平仓</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inst-stats-block">
|
||||||
|
<div class="inst-stats-block-title">盈亏构成</div>
|
||||||
|
<div class="inst-stats-stacked-bar">
|
||||||
|
<div class="inst-stats-stacked-fill inst-stats-stacked-fill--profit" style="width: {{ '%.1f'|format(profit_bar_w) }}%"></div>
|
||||||
|
<div class="inst-stats-stacked-fill inst-stats-stacked-fill--loss" style="width: {{ '%.1f'|format(loss_bar_w) }}%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="inst-stats-bar-labels">
|
||||||
|
<span class="pos-pnl-profit">盈利 {{ funds_fmt(profit_sum) }}U</span>
|
||||||
|
<span class="pos-pnl-loss">亏损 {{ funds_fmt(loss_sum) }}U</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inst-stats-block inst-stats-block--risk">
|
||||||
|
<div class="inst-stats-risk-grid">
|
||||||
|
<div class="inst-stats-risk-item">
|
||||||
|
<span class="k">最大回撤</span>
|
||||||
|
<span class="v pos-pnl-loss">{{ funds_fmt(s.max_drawdown_u) }}U</span>
|
||||||
|
</div>
|
||||||
|
<div class="inst-stats-risk-item">
|
||||||
|
<span class="k">连续亏损</span>
|
||||||
|
<span class="v">{{ s.consecutive_losses }} 笔</span>
|
||||||
|
</div>
|
||||||
|
<div class="inst-stats-risk-item">
|
||||||
|
<span class="k">最长连亏日</span>
|
||||||
|
<span class="v">{{ s.max_loss_streak_days }} 天</span>
|
||||||
|
</div>
|
||||||
|
<div class="inst-stats-risk-item">
|
||||||
|
<span class="k">最大亏损日</span>
|
||||||
|
<span class="v">{% if s.worst_day %}{{ s.worst_day }} ({{ funds_fmt(s.worst_day_pnl) }}U){% else %}—{% endif %}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="inst-stats-empty">当前区间暂无平仓数据</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<details class="inst-stats-details" open>
|
||||||
|
<summary>详细指标</summary>
|
||||||
|
<div class="stats-detail">
|
||||||
|
<div class="stat-item"><div class="label">开单次数</div><div class="value">{{ s.opens_count }}</div></div>
|
||||||
|
<div class="stat-item"><div class="label">平仓笔数</div><div class="value">{{ s.closed_count }}</div></div>
|
||||||
|
<div class="stat-item"><div class="label">胜率</div><div class="value">{% if s.win_rate_pct is not none %}{{ s.win_rate_pct }}%{% else %}-{% endif %}</div></div>
|
||||||
|
<div class="stat-item"><div class="label">净盈亏(U)</div><div class="value">{{ funds_fmt(s.net_pnl_u) }}</div></div>
|
||||||
|
<div class="stat-item"><div class="label">亏损额合计(U)</div><div class="value">{{ funds_fmt(s.loss_sum_u) }}</div></div>
|
||||||
|
<div class="stat-item"><div class="label">单笔最大亏损(U)</div><div class="value">{% if s.max_single_loss is not none %}{{ funds_fmt(s.max_single_loss) }}{% else %}-{% endif %}</div></div>
|
||||||
|
<div class="stat-item"><div class="label">单笔最大盈利(U)</div><div class="value">{% if s.max_single_profit is not none %}{{ funds_fmt(s.max_single_profit) }}{% else %}-{% endif %}</div></div>
|
||||||
|
<div class="stat-item"><div class="label">最大回撤(U)</div><div class="value">{{ funds_fmt(s.max_drawdown_u) }}</div></div>
|
||||||
|
<div class="stat-item"><div class="label">当前连续亏损笔数</div><div class="value">{{ s.consecutive_losses }}</div></div>
|
||||||
|
<div class="stat-item"><div class="label">最长连续亏损(交易日)</div><div class="value">{{ s.max_loss_streak_days }} 天</div></div>
|
||||||
|
<div class="stat-item"><div class="label">期内最大亏损日</div><div class="value">{% if s.worst_day %}{{ s.worst_day }}({{ funds_fmt(s.worst_day_pnl) }}U){% else %}-{% endif %}</div></div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -519,9 +581,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% for seg in stats_bundle.segments %}
|
{% for seg in stats_bundle.segments %}
|
||||||
<div class="stats-segment-block stats-segment-panel" data-stats-segment="{{ seg.key }}"{% if not loop.first %} style="display:none"{% endif %}>
|
<div class="stats-segment-block stats-segment-panel" data-stats-segment="{{ seg.key }}"{% if not loop.first %} style="display:none"{% endif %}>
|
||||||
{{ period_stats("日统计", seg.day) }}
|
<div class="stats-period-tabs" role="tablist" aria-label="统计周期">
|
||||||
{{ period_stats("周统计", seg.week) }}
|
<button type="button" class="stats-period-tab active" data-stats-period="day" role="tab" aria-selected="true">日统计</button>
|
||||||
{{ period_stats("月统计", seg.month) }}
|
<button type="button" class="stats-period-tab" data-stats-period="week" role="tab" aria-selected="false">周统计</button>
|
||||||
|
<button type="button" class="stats-period-tab" data-stats-period="month" role="tab" aria-selected="false">月统计</button>
|
||||||
|
</div>
|
||||||
|
{{ period_stats_pane("day", seg.day) }}
|
||||||
|
{{ period_stats_pane("week", seg.week) }}
|
||||||
|
{{ period_stats_pane("month", seg.month) }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@@ -562,6 +629,7 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj
|
|||||||
<script src="/static/manual_order_rr_preview.js?v=5"></script>
|
<script src="/static/manual_order_rr_preview.js?v=5"></script>
|
||||||
<script src="/static/symbol_live_price.js?v=2"></script>
|
<script src="/static/symbol_live_price.js?v=2"></script>
|
||||||
<script src="/static/strategy_roll.js?v=6"></script>
|
<script src="/static/strategy_roll.js?v=6"></script>
|
||||||
|
<script src="/static/instance_stats.js?v=1"></script>
|
||||||
<script>
|
<script>
|
||||||
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
|
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
|
||||||
const JOURNAL_ORDER_TYPE_OPTIONS = {{ order_type_options | tojson }};
|
const JOURNAL_ORDER_TYPE_OPTIONS = {{ order_type_options | tojson }};
|
||||||
@@ -1153,29 +1221,6 @@ function recomputeJournalRealRr(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function switchStatsSegment(){
|
|
||||||
const sel = document.getElementById("stats-segment-select");
|
|
||||||
if(!sel) return;
|
|
||||||
const key = sel.value;
|
|
||||||
document.querySelectorAll(".stats-segment-panel").forEach(p=>{
|
|
||||||
p.style.display = p.getAttribute("data-stats-segment") === key ? "block" : "none";
|
|
||||||
});
|
|
||||||
const q = new URLSearchParams(window.location.search);
|
|
||||||
q.set("stats_segment", key);
|
|
||||||
const qs = q.toString();
|
|
||||||
history.replaceState(null, "", qs ? (window.location.pathname + "?" + qs) : window.location.pathname);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initStatsSegmentFromUrl(){
|
|
||||||
const sel = document.getElementById("stats-segment-select");
|
|
||||||
if(!sel) return;
|
|
||||||
const key = new URLSearchParams(window.location.search).get("stats_segment");
|
|
||||||
if(key && sel.querySelector('option[value="' + key.replace(/"/g, "") + '"]')){
|
|
||||||
sel.value = key;
|
|
||||||
}
|
|
||||||
switchStatsSegment();
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleStatsCard(){
|
function toggleStatsCard(){
|
||||||
const card = document.getElementById("stats-card");
|
const card = document.getElementById("stats-card");
|
||||||
const btn = document.getElementById("stats-toggle-btn");
|
const btn = document.getElementById("stats-toggle-btn");
|
||||||
|
|||||||
@@ -7,39 +7,40 @@ ROOT = Path(__file__).resolve().parent.parent
|
|||||||
SRC = ROOT / "lib" / "instance" / "templates" / "index.html"
|
SRC = ROOT / "lib" / "instance" / "templates" / "index.html"
|
||||||
OUT = ROOT / "lib" / "instance" / "templates" / "embed_page_fragment.html"
|
OUT = ROOT / "lib" / "instance" / "templates" / "embed_page_fragment.html"
|
||||||
|
|
||||||
GRID_START = ' <div class="grid">'
|
GRID_START = " <div class=\"grid\">"
|
||||||
STATS_START = ' <div class="card full stats-card'
|
|
||||||
|
|
||||||
|
|
||||||
def _slice_between(lines: list[str], start: str, end: str | None) -> list[str]:
|
def _find_line(lines: list[str], predicate, *, start: int = 0) -> int:
|
||||||
try:
|
for idx in range(start, len(lines)):
|
||||||
i = next(idx for idx, line in enumerate(lines) if line == start)
|
if predicate(lines[idx]):
|
||||||
except StopIteration:
|
return idx
|
||||||
raise SystemExit(f"marker not found: {start!r}")
|
raise SystemExit("marker not found")
|
||||||
if end is None:
|
|
||||||
return lines[i:]
|
|
||||||
try:
|
|
||||||
j = next(idx for idx, line in enumerate(lines[i + 1 :], i + 1) if line.startswith(end))
|
|
||||||
except StopIteration:
|
|
||||||
raise SystemExit(f"end marker not found: {end!r}")
|
|
||||||
return lines[i:j]
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
lines = SRC.read_text(encoding="utf-8").splitlines()
|
lines = SRC.read_text(encoding="utf-8").splitlines()
|
||||||
macro_start = next(i for i, l in enumerate(lines) if l.startswith("{% macro period_stats"))
|
macro_start = _find_line(lines, lambda l: "macro period_stats_pane" in l)
|
||||||
macro_end = next(i for i, l in enumerate(lines) if l.strip() == "{% endmacro %}")
|
macro_end = _find_line(lines, lambda l: l.strip() == "{% endmacro %}", start=macro_start)
|
||||||
macro_body = lines[macro_start : macro_end + 1]
|
macro_body = lines[macro_start : macro_end + 1]
|
||||||
|
|
||||||
grid_block = _slice_between(lines, GRID_START, STATS_START)
|
grid_start = _find_line(lines, lambda l: l == GRID_START)
|
||||||
# strip outer .grid wrapper; fragment adds its own
|
panel_start = _find_line(lines, lambda l: "{% if page == 'env_config' %}" in l)
|
||||||
if grid_block and grid_block[0] == GRID_START:
|
stats_card_line = _find_line(lines, lambda l: 'id="stats-card"' in l)
|
||||||
grid_block = grid_block[1:]
|
stats_start = stats_card_line
|
||||||
if grid_block and grid_block[-1].strip() == "</div>":
|
while stats_start > 0 and lines[stats_start].strip() != "{% if page == 'stats' %}":
|
||||||
# only remove closing div if it closes .grid (heuristic: last line before stats)
|
stats_start -= 1
|
||||||
pass
|
if lines[stats_start].strip() != "{% if page == 'stats' %}":
|
||||||
|
raise SystemExit("stats if-block not found")
|
||||||
|
stats_end = _find_line(lines, lambda l: l.strip() == "{% endif %}", start=stats_start + 1)
|
||||||
|
|
||||||
stats_block = _slice_between(lines, STATS_START, " </div>")
|
grid_block = lines[grid_start + 1 : panel_start]
|
||||||
|
while grid_block and not grid_block[-1].strip():
|
||||||
|
grid_block.pop()
|
||||||
|
if grid_block and grid_block[-1].strip() == "</div>":
|
||||||
|
grid_block.pop()
|
||||||
|
|
||||||
|
panel_block = lines[panel_start:stats_start]
|
||||||
|
stats_block = lines[stats_start : stats_end + 1]
|
||||||
|
|
||||||
out_lines = [
|
out_lines = [
|
||||||
"{# Hub iframe tab fragment — shared via embed_templates #}",
|
"{# Hub iframe tab fragment — shared via embed_templates #}",
|
||||||
@@ -47,6 +48,7 @@ def main() -> None:
|
|||||||
'<div class="grid">',
|
'<div class="grid">',
|
||||||
*grid_block,
|
*grid_block,
|
||||||
"</div>",
|
"</div>",
|
||||||
|
*panel_block,
|
||||||
*stats_block,
|
*stats_block,
|
||||||
]
|
]
|
||||||
text = "\n".join(out_lines).rstrip() + "\n"
|
text = "\n".join(out_lines).rstrip() + "\n"
|
||||||
|
|||||||
Reference in New Issue
Block a user