Add collapsible archive stats charts and calendar sections.
Show win rate ring, PnL and sick bars, exchange breakdown, hold-time comparison, and cumulative curve below the stats table; wrap the trading calendar in a foldable panel. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7211,6 +7211,149 @@ body.funds-fullscreen-open {
|
||||
.archive-stats-table tr.archive-stats-total td {
|
||||
background: color-mix(in srgb, var(--accent) 6%, transparent);
|
||||
}
|
||||
.archive-stats-table .pnl-neg {
|
||||
color: var(--loss);
|
||||
}
|
||||
.archive-stats-viz-section {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.archive-stats-charts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
.archive-viz-grid {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 10px 14px;
|
||||
align-items: center;
|
||||
}
|
||||
.archive-viz-ring-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.archive-viz-ring {
|
||||
--win-pct: 0;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
border-radius: 50%;
|
||||
background: conic-gradient(
|
||||
var(--profit) 0 calc(var(--win-pct) * 1%),
|
||||
var(--loss) calc(var(--win-pct) * 1%) 100%
|
||||
);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
.archive-viz-ring::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--inset-surface);
|
||||
}
|
||||
.archive-viz-ring-label {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.archive-viz-caption,
|
||||
.archive-viz-block-title {
|
||||
font-size: 0.66rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
.archive-viz-block-title {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.archive-viz-bars,
|
||||
.archive-viz-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
.archive-viz-bar-row {
|
||||
display: grid;
|
||||
grid-template-columns: 2.4em 1fr auto;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
.archive-viz-bar-row .k {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.archive-viz-bar-row .v {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.archive-viz-bar-track {
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
.archive-viz-bar-fill {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
border-radius: 999px;
|
||||
transition: width 0.25s ease;
|
||||
}
|
||||
.archive-viz-bar-fill--profit {
|
||||
background: linear-gradient(90deg, #2f9f62, var(--profit));
|
||||
}
|
||||
.archive-viz-bar-fill--loss {
|
||||
background: linear-gradient(90deg, #c44a4a, var(--loss));
|
||||
}
|
||||
.archive-viz-bar-fill--sick {
|
||||
background: linear-gradient(90deg, #9b59b6, #c084fc);
|
||||
}
|
||||
.archive-viz-empty {
|
||||
margin: 0;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
.archive-cum-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.archive-cum-end {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.archive-cum-chart {
|
||||
width: 100%;
|
||||
height: 88px;
|
||||
display: block;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.archive-cum-zero {
|
||||
stroke: rgba(255, 255, 255, 0.12);
|
||||
stroke-width: 1;
|
||||
}
|
||||
.archive-cum-line {
|
||||
fill: none;
|
||||
stroke: #5b8cff;
|
||||
stroke-width: 2;
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
.archive-calendar-section {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.archive-calendar-section .trade-cal-wrap {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.archive-stats-table .pnl-pos {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
const elBtnSync = document.getElementById("archive-btn-sync");
|
||||
const elStatus = document.getElementById("archive-status");
|
||||
const elStats = document.getElementById("archive-stats");
|
||||
const elStatsCharts = document.getElementById("archive-stats-charts");
|
||||
const elStatsVizSub = document.getElementById("archive-stats-viz-sub");
|
||||
const elCalSummarySub = document.getElementById("archive-cal-summary-sub");
|
||||
const elCalendarWrap = document.getElementById("archive-calendar-wrap");
|
||||
const elCalendar = document.getElementById("archive-calendar");
|
||||
const elCalTitle = document.getElementById("archive-cal-title");
|
||||
@@ -552,6 +555,11 @@
|
||||
if (!cal) return;
|
||||
cal.selectedDay = selectedCalendarDay;
|
||||
await cal.load();
|
||||
if (elCalSummarySub && cal.monthPnlTotal != null) {
|
||||
const pnl = Number(cal.monthPnlTotal) || 0;
|
||||
const sign = pnl > 0 ? "+" : "";
|
||||
elCalSummarySub.textContent = cal.year + "年" + cal.month + "月 " + sign + pnl.toFixed(2) + "U";
|
||||
}
|
||||
}
|
||||
|
||||
function renderStats() {
|
||||
@@ -593,6 +601,185 @@
|
||||
"</tr></thead><tbody>" +
|
||||
rows +
|
||||
"</tbody></table>";
|
||||
renderStatsCharts();
|
||||
}
|
||||
|
||||
function fmtDurationMinutes(minutes) {
|
||||
if (minutes == null || minutes === "" || Number.isNaN(Number(minutes))) return "—";
|
||||
let m = Math.max(0, Math.round(Number(minutes)));
|
||||
if (m < 60) return m + "分";
|
||||
const h = Math.floor(m / 60);
|
||||
const rm = m % 60;
|
||||
if (h < 24) return rm ? h + "时" + rm + "分" : h + "时";
|
||||
const d = Math.floor(h / 24);
|
||||
const rh = h % 24;
|
||||
return rh ? d + "天" + rh + "时" : d + "天";
|
||||
}
|
||||
|
||||
function sumTradePnlSides(trades) {
|
||||
let profit = 0;
|
||||
let loss = 0;
|
||||
(trades || []).forEach(function (t) {
|
||||
const pnl = Number(t.pnl_amount);
|
||||
if (!Number.isFinite(pnl)) return;
|
||||
if (pnl > 0.0001) profit += pnl;
|
||||
else if (pnl < -0.0001) loss += Math.abs(pnl);
|
||||
});
|
||||
return { profit: profit, loss: loss };
|
||||
}
|
||||
|
||||
function avgHoldMinutes(trades, side) {
|
||||
const vals = [];
|
||||
(trades || []).forEach(function (t) {
|
||||
const pnl = Number(t.pnl_amount);
|
||||
const hold = Number(t.hold_minutes);
|
||||
if (!Number.isFinite(hold) || hold < 0) return;
|
||||
if (side === "win" && pnl > 0.0001) vals.push(hold);
|
||||
if (side === "loss" && pnl < -0.0001) vals.push(hold);
|
||||
});
|
||||
if (!vals.length) return null;
|
||||
return Math.round(vals.reduce(function (a, b) { return a + b; }, 0) / vals.length);
|
||||
}
|
||||
|
||||
function buildCumulativeSeries(trades) {
|
||||
const byDay = {};
|
||||
(trades || []).forEach(function (t) {
|
||||
const raw = t.closed_at || "";
|
||||
const day = String(raw).slice(0, 10);
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) return;
|
||||
byDay[day] = (byDay[day] || 0) + Number(t.pnl_amount || 0);
|
||||
});
|
||||
const days = Object.keys(byDay).sort();
|
||||
let cum = 0;
|
||||
return days.map(function (day) {
|
||||
cum += byDay[day];
|
||||
return { day: day, pnl: byDay[day], cum: cum };
|
||||
});
|
||||
}
|
||||
|
||||
function renderCumulativeChart(series) {
|
||||
if (!series.length) {
|
||||
return '<p class="archive-viz-empty muted">当前区间暂无平仓数据</p>';
|
||||
}
|
||||
const w = 360;
|
||||
const h = 88;
|
||||
const padX = 12;
|
||||
const padY = 10;
|
||||
const vals = series.map(function (s) { return s.cum; });
|
||||
const minV = Math.min(0, ...vals);
|
||||
const maxV = Math.max(0, ...vals);
|
||||
const range = maxV - minV || 1;
|
||||
const innerW = w - padX * 2;
|
||||
const innerH = h - padY * 2;
|
||||
const zeroY = padY + innerH - ((0 - minV) / range) * innerH;
|
||||
const pts = series
|
||||
.map(function (s, i) {
|
||||
const x = padX + (i / Math.max(series.length - 1, 1)) * innerW;
|
||||
const y = padY + innerH - ((s.cum - minV) / range) * innerH;
|
||||
return x.toFixed(1) + "," + y.toFixed(1);
|
||||
})
|
||||
.join(" ");
|
||||
const last = series[series.length - 1];
|
||||
const lastCls = last.cum >= 0 ? "pnl-pos" : "pnl-neg";
|
||||
const sign = last.cum > 0 ? "+" : "";
|
||||
return (
|
||||
'<div class="archive-cum-head">' +
|
||||
'<span class="archive-viz-block-title">累计盈亏</span>' +
|
||||
'<span class="archive-cum-end ' + lastCls + '">' + sign + last.cum.toFixed(2) + "U</span>" +
|
||||
"</div>" +
|
||||
'<svg class="archive-cum-chart" viewBox="0 0 ' + w + " " + h + '" preserveAspectRatio="none" aria-hidden="true">' +
|
||||
'<line x1="' + padX + '" y1="' + zeroY.toFixed(1) + '" x2="' + (w - padX) + '" y2="' + zeroY.toFixed(1) + '" class="archive-cum-zero" />' +
|
||||
'<polyline points="' + pts + '" class="archive-cum-line" />' +
|
||||
"</svg>"
|
||||
);
|
||||
}
|
||||
|
||||
function barRow(label, valueLabel, pct, fillCls) {
|
||||
return (
|
||||
'<div class="archive-viz-bar-row">' +
|
||||
'<span class="k">' + esc(label) + "</span>" +
|
||||
'<div class="archive-viz-bar-track"><div class="archive-viz-bar-fill ' + fillCls + '" style="width:' + Math.max(0, Math.min(100, pct)).toFixed(1) + '%"></div></div>' +
|
||||
'<span class="v">' + esc(valueLabel) + "</span>" +
|
||||
"</div>"
|
||||
);
|
||||
}
|
||||
|
||||
function renderStatsCharts() {
|
||||
if (!elStatsCharts) return;
|
||||
const st = dailyStats || { open_count: 0, by_exchange: {} };
|
||||
const openN = st.open_count || 0;
|
||||
const winRate = openN ? Number(st.win_rate) || 0 : 0;
|
||||
const sides = sumTradePnlSides(dailyTrades);
|
||||
const pnlTotal = sides.profit + sides.loss;
|
||||
const sickN = st.sick_count || 0;
|
||||
const sickPct = openN ? (sickN / openN) * 100 : 0;
|
||||
const winHold = avgHoldMinutes(dailyTrades, "win");
|
||||
const lossHold = avgHoldMinutes(dailyTrades, "loss");
|
||||
const holdMax = Math.max(winHold || 0, lossHold || 0);
|
||||
const byEx = st.by_exchange || {};
|
||||
const exKeys = Object.keys(byEx).sort();
|
||||
|
||||
if (elStatsVizSub) {
|
||||
const pnl = Number(st.pnl_total) || 0;
|
||||
const sign = pnl > 0 ? "+" : "";
|
||||
elStatsVizSub.textContent = openN
|
||||
? "胜率 " + winRate.toFixed(0) + "% · " + sign + pnl.toFixed(2) + "U"
|
||||
: "暂无平仓";
|
||||
}
|
||||
|
||||
let exBars = "";
|
||||
if (exKeys.length) {
|
||||
const maxAbs = Math.max.apply(
|
||||
null,
|
||||
exKeys.map(function (ex) {
|
||||
return Math.abs(Number(byEx[ex].pnl_total) || 0);
|
||||
}).concat([0.0001])
|
||||
);
|
||||
exBars = exKeys
|
||||
.map(function (ex) {
|
||||
const pnl = Number(byEx[ex].pnl_total) || 0;
|
||||
const pct = (Math.abs(pnl) / maxAbs) * 100;
|
||||
const cls = pnl >= 0 ? "archive-viz-bar-fill--profit" : "archive-viz-bar-fill--loss";
|
||||
const sign = pnl > 0 ? "+" : "";
|
||||
return barRow(exchangeLabel(ex), sign + pnl.toFixed(2) + "U", pct, cls);
|
||||
})
|
||||
.join("");
|
||||
} else {
|
||||
exBars = '<p class="archive-viz-empty muted">暂无分交易所数据</p>';
|
||||
}
|
||||
|
||||
const cumSeries = buildCumulativeSeries(dailyTrades);
|
||||
elStatsCharts.innerHTML =
|
||||
'<div class="archive-viz-grid">' +
|
||||
'<div class="archive-viz-ring-wrap">' +
|
||||
'<div class="archive-viz-ring" style="--win-pct:' + winRate.toFixed(1) + '">' +
|
||||
'<span class="archive-viz-ring-label">' + (openN ? winRate.toFixed(0) + "%" : "—") + "</span>" +
|
||||
"</div>" +
|
||||
'<span class="archive-viz-caption">胜率</span>' +
|
||||
"</div>" +
|
||||
'<div class="archive-viz-bars">' +
|
||||
(pnlTotal > 0
|
||||
? barRow("盈利", sides.profit.toFixed(2) + "U", (sides.profit / pnlTotal) * 100, "archive-viz-bar-fill--profit") +
|
||||
barRow("亏损", sides.loss.toFixed(2) + "U", (sides.loss / pnlTotal) * 100, "archive-viz-bar-fill--loss")
|
||||
: barRow("盈利", "—", 0, "archive-viz-bar-fill--profit") +
|
||||
barRow("亏损", "—", 0, "archive-viz-bar-fill--loss")) +
|
||||
barRow("犯病", sickN + " 笔 · " + sickPct.toFixed(1) + "%", sickPct, "archive-viz-bar-fill--sick") +
|
||||
"</div>" +
|
||||
"</div>" +
|
||||
'<div class="archive-viz-block archive-viz-block--exchange">' +
|
||||
'<div class="archive-viz-block-title">分交易所盈亏</div>' +
|
||||
exBars +
|
||||
"</div>" +
|
||||
(holdMax > 0
|
||||
? '<div class="archive-viz-block archive-viz-block--hold">' +
|
||||
'<div class="archive-viz-block-title">持仓时长对比</div>' +
|
||||
barRow("盈单", fmtDurationMinutes(winHold), ((winHold || 0) / holdMax) * 100, "archive-viz-bar-fill--profit") +
|
||||
barRow("亏单", fmtDurationMinutes(lossHold), ((lossHold || 0) / holdMax) * 100, "archive-viz-bar-fill--loss") +
|
||||
"</div>"
|
||||
: "") +
|
||||
'<div class="archive-viz-block archive-viz-block--cum">' +
|
||||
renderCumulativeChart(cumSeries) +
|
||||
"</div>";
|
||||
}
|
||||
|
||||
function quotePreview(text) {
|
||||
|
||||
@@ -470,6 +470,10 @@
|
||||
<h2>统计分析</h2>
|
||||
</div>
|
||||
<div id="archive-stats" class="archive-stats-bar"></div>
|
||||
<details class="archive-acc-section archive-stats-viz-section" id="archive-stats-viz-section" open>
|
||||
<summary class="archive-acc-summary">图表概览 <span id="archive-stats-viz-sub" class="archive-acc-sub">—</span></summary>
|
||||
<div id="archive-stats-charts" class="archive-stats-charts"></div>
|
||||
</details>
|
||||
</section>
|
||||
<div class="archive-layout">
|
||||
<aside class="archive-quotes-panel">
|
||||
@@ -485,14 +489,17 @@
|
||||
<div id="archive-quotes-list" class="archive-quotes-list"></div>
|
||||
</aside>
|
||||
<main class="archive-main-panel">
|
||||
<div id="archive-calendar-wrap" class="trade-cal-wrap">
|
||||
<div class="trade-cal-head">
|
||||
<button type="button" id="archive-cal-prev" class="ghost" title="上一月">‹</button>
|
||||
<span id="archive-cal-title" class="trade-cal-title"></span>
|
||||
<button type="button" id="archive-cal-next" class="ghost" title="下一月">›</button>
|
||||
<details class="archive-acc-section archive-calendar-section" id="archive-calendar-section" open>
|
||||
<summary class="archive-acc-summary">交易日历 <span id="archive-cal-summary-sub" class="archive-acc-sub">—</span></summary>
|
||||
<div id="archive-calendar-wrap" class="trade-cal-wrap">
|
||||
<div class="trade-cal-head">
|
||||
<button type="button" id="archive-cal-prev" class="ghost" title="上一月">‹</button>
|
||||
<span id="archive-cal-title" class="trade-cal-title"></span>
|
||||
<button type="button" id="archive-cal-next" class="ghost" title="下一月">›</button>
|
||||
</div>
|
||||
<div id="archive-calendar" class="trade-cal-grid-host" role="grid" aria-label="交易日历"></div>
|
||||
</div>
|
||||
<div id="archive-calendar" class="trade-cal-grid-host" role="grid" aria-label="交易日历"></div>
|
||||
</div>
|
||||
</details>
|
||||
<details id="archive-chart-section" class="archive-acc-section archive-chart-section archive-panel-desktop">
|
||||
<summary class="archive-acc-summary">K 线图表 <span id="archive-chart-title" class="archive-acc-sub">—</span></summary>
|
||||
<div class="archive-chart-toolbar toolbar">
|
||||
@@ -1222,7 +1229,7 @@
|
||||
<script src="/assets/plan.js?v=20260614-plan-refresh"></script>
|
||||
<script src="/assets/calculator.js?v=3"></script>
|
||||
<script src="/assets/trade_stats_calendar.js?v=3"></script>
|
||||
<script src="/assets/archive.js?v=20260626-archive-layout"></script>
|
||||
<script src="/assets/archive.js?v=20260710-archive-viz"></script>
|
||||
<script src="/assets/funds.js?v=20260707-hub-options-funds"></script>
|
||||
<script src="/assets/dashboard.js?v=20260708-options-expiry-cd"></script>
|
||||
<script src="/assets/strategy.js?v=3"></script>
|
||||
|
||||
Reference in New Issue
Block a user