Add tabbed period stats with lightweight charts on instance pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-10 13:27:02 +08:00
parent 0ce14c8822
commit 6aaa328a24
8 changed files with 826 additions and 535 deletions
+89 -44
View File
@@ -16,8 +16,8 @@
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
<link rel="manifest" href="/static/icons/manifest.webmanifest">
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
<link rel="stylesheet" href="/static/instance_page.css?v=1">
<link rel="stylesheet" href="/static/instance_theme.css?v=77">
<link rel="stylesheet" href="/static/instance_page.css?v=2">
<link rel="stylesheet" href="/static/instance_theme.css?v=78">
</head>
<body
@@ -30,23 +30,85 @@
data-full-margin-buffer="{{ full_margin_buffer_ratio }}"
data-price-refresh-ms="{{ price_refresh_seconds * 1000 }}"
>
{% macro period_stats(title, s) %}
<div class="stats-period-block">
<h3>{{ title }}</h3>
<div class="sub">{{ s.range_label }}</div>
<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>
{% macro period_stats_pane(period_key, s) %}
{% set win_pct = s.win_rate_pct if s.win_rate_pct is not none else 0 %}
{% set profit_sum = (s.net_pnl_u + s.loss_sum_u) if s.closed_count else 0 %}
{% set loss_sum = s.loss_sum_u %}
{% set pnl_total = profit_sum + loss_sum %}
{% set profit_bar_w = (profit_sum / pnl_total * 100) if pnl_total > 0 else 0 %}
{% set loss_bar_w = (loss_sum / pnl_total * 100) if pnl_total > 0 else 0 %}
{% 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="stats-period-pane" data-stats-period="{{ period_key }}" role="tabpanel"{% if period_key != 'day' %} hidden{% endif %}>
<div class="stats-period-range">{{ s.range_label }}</div>
<div class="inst-stats-viz">
{% if s.closed_count %}
<div class="inst-stats-kpis">
<div class="inst-stats-kpi inst-stats-kpi--pnl">
<span class="inst-stats-kpi-val {{ net_cls }}">{% if s.net_pnl_u > 0 %}+{% endif %}{{ funds_fmt(s.net_pnl_u) }}U</span>
<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>
<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>
{% endmacro %}
<div class="container">
@@ -519,9 +581,14 @@
</div>
{% 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 %}>
{{ period_stats("日统计", seg.day) }}
{{ period_stats("周统计", seg.week) }}
{{ period_stats("月统计", seg.month) }}
<div class="stats-period-tabs" role="tablist" aria-label="统计周期">
<button type="button" class="stats-period-tab active" data-stats-period="day" role="tab" aria-selected="true">日统计</button>
<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>
{% endfor %}
</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/symbol_live_price.js?v=2"></script>
<script src="/static/strategy_roll.js?v=6"></script>
<script src="/static/instance_stats.js?v=1"></script>
<script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_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(){
const card = document.getElementById("stats-card");
const btn = document.getElementById("stats-toggle-btn");