Improve archive stats chart layout and fix invisible bar colors.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-10 11:58:27 +08:00
parent f3af6e2a53
commit daf023b403
3 changed files with 376 additions and 78 deletions
+210 -36
View File
@@ -7220,20 +7220,46 @@ body.funds-fullscreen-open {
.archive-stats-charts {
display: flex;
flex-direction: column;
gap: 10px;
gap: 12px;
padding-top: 4px;
}
.archive-viz-grid {
.archive-viz-kpis {
display: grid;
grid-template-columns: auto 1fr;
gap: 10px 14px;
align-items: center;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.archive-viz-ring-wrap {
.archive-viz-kpi {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
padding: 10px 8px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
min-width: 0;
}
.archive-viz-kpi-val {
font-size: 1.05rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
line-height: 1.1;
}
.archive-viz-kpi--pnl .archive-viz-kpi-val {
font-size: 1.2rem;
}
.archive-viz-kpi-lbl {
font-size: 0.66rem;
color: var(--muted);
text-align: center;
}
.archive-viz-kpi--win .archive-viz-ring {
width: 52px;
height: 52px;
}
.archive-viz-kpi--win .archive-viz-ring::before {
inset: 6px;
}
.archive-viz-ring {
--win-pct: 0;
@@ -7241,8 +7267,8 @@ body.funds-fullscreen-open {
height: 68px;
border-radius: 50%;
background: conic-gradient(
var(--profit) 0 calc(var(--win-pct) * 1%),
var(--loss) calc(var(--win-pct) * 1%) 100%
#4cd97f 0 calc(var(--win-pct) * 1%),
#ff6b6b calc(var(--win-pct) * 1%) 100%
);
display: flex;
align-items: center;
@@ -7254,7 +7280,7 @@ body.funds-fullscreen-open {
position: absolute;
inset: 8px;
border-radius: 50%;
background: var(--inset-surface);
background: var(--inset-surface, #141923);
}
.archive-viz-ring-label {
position: relative;
@@ -7263,42 +7289,153 @@ body.funds-fullscreen-open {
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.archive-viz-caption,
.archive-viz-block-title {
font-size: 0.66rem;
font-size: 0.68rem;
color: var(--muted);
margin-bottom: 6px;
font-weight: 600;
}
.archive-viz-block-title {
margin-bottom: 4px;
}
.archive-viz-bars,
.archive-viz-block {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
padding: 10px;
border-radius: 10px;
background: rgba(0, 0, 0, 0.12);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.archive-viz-bar-row {
.archive-viz-stacked {
display: flex;
height: 22px;
border-radius: 6px;
overflow: hidden;
background: rgba(255, 255, 255, 0.04);
}
.archive-viz-stacked-seg {
display: flex;
align-items: center;
justify-content: center;
min-width: 2px;
font-size: 0.62rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.92);
white-space: nowrap;
overflow: hidden;
}
.archive-viz-stacked-seg--profit {
background: linear-gradient(90deg, #2f9f62, #4cd97f);
}
.archive-viz-stacked-seg--loss {
background: linear-gradient(90deg, #c44a4a, #ff6b6b);
}
.archive-viz-stacked-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
font-size: 0.68rem;
}
.archive-viz-legend::before {
content: "";
display: inline-block;
width: 8px;
height: 8px;
border-radius: 2px;
margin-right: 4px;
vertical-align: middle;
}
.archive-viz-legend--profit::before {
background: #4cd97f;
}
.archive-viz-legend--loss::before {
background: #ff6b6b;
}
.archive-viz-legend--net {
font-weight: 700;
}
.archive-viz-div-row {
display: grid;
grid-template-columns: 2.4em 1fr auto;
gap: 6px;
grid-template-columns: minmax(4.5em, 7.5em) 1fr auto;
gap: 8px;
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;
.archive-viz-div-row .k {
opacity: 0.9;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.archive-viz-bar-track {
height: 8px;
border-radius: 999px;
.archive-viz-div-row .v {
font-size: 0.72rem;
font-weight: 700;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.archive-viz-div-track {
position: relative;
height: 12px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.06);
overflow: hidden;
}
.archive-viz-div-mid {
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 1px;
background: rgba(255, 255, 255, 0.2);
transform: translateX(-50%);
z-index: 1;
}
.archive-viz-div-fill {
position: absolute;
top: 0;
bottom: 0;
min-width: 2px;
border-radius: 2px;
}
.archive-viz-div-fill--profit {
background: linear-gradient(90deg, #2f9f62, #4cd97f);
}
.archive-viz-div-fill--loss {
background: linear-gradient(270deg, #c44a4a, #ff6b6b);
}
.archive-viz-hold-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.archive-viz-hold-card {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.03);
}
.archive-viz-hold-val {
font-size: 0.95rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.archive-viz-hold-card--win .archive-viz-hold-val {
color: #4cd97f;
}
.archive-viz-hold-card--loss .archive-viz-hold-val {
color: #ff6b6b;
}
.archive-viz-hold-lbl {
font-size: 0.64rem;
color: var(--muted);
}
.archive-viz-bar-track {
height: 10px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
overflow: hidden;
}
.archive-viz-bar-fill {
height: 100%;
width: 0;
@@ -7306,13 +7443,10 @@ body.funds-fullscreen-open {
transition: width 0.25s ease;
}
.archive-viz-bar-fill--profit {
background: linear-gradient(90deg, #2f9f62, var(--profit));
background: linear-gradient(90deg, #2f9f62, #4cd97f);
}
.archive-viz-bar-fill--loss {
background: linear-gradient(90deg, #c44a4a, var(--loss));
}
.archive-viz-bar-fill--sick {
background: linear-gradient(90deg, #9b59b6, #c084fc);
background: linear-gradient(90deg, #c44a4a, #ff6b6b);
}
.archive-viz-empty {
margin: 0;
@@ -7332,22 +7466,62 @@ body.funds-fullscreen-open {
}
.archive-cum-chart {
width: 100%;
height: 88px;
height: 110px;
display: block;
border-radius: 8px;
background: rgba(0, 0, 0, 0.12);
background: rgba(0, 0, 0, 0.15);
}
.archive-cum-ylabel,
.archive-cum-xlabel {
fill: rgba(255, 255, 255, 0.45);
font-size: 9px;
font-family: inherit;
}
.archive-cum-zero {
stroke: rgba(255, 255, 255, 0.12);
stroke: rgba(255, 255, 255, 0.15);
stroke-width: 1;
stroke-dasharray: 3 3;
}
.archive-cum-area {
opacity: 0.18;
}
.archive-cum-area.archive-cum-line--up {
fill: #4cd97f;
}
.archive-cum-area.archive-cum-line--down {
fill: #ff6b6b;
}
.archive-cum-line {
fill: none;
stroke: #5b8cff;
stroke-width: 2;
stroke-linejoin: round;
stroke-linecap: round;
}
.archive-cum-line--up {
stroke: #4cd97f;
}
.archive-cum-line--down {
stroke: #ff6b6b;
}
.archive-cum-dot {
stroke: rgba(0, 0, 0, 0.35);
stroke-width: 1;
}
.archive-cum-dot.archive-cum-line--up {
fill: #4cd97f;
}
.archive-cum-dot.archive-cum-line--down {
fill: #ff6b6b;
}
@media (max-width: 520px) {
.archive-viz-kpis {
grid-template-columns: 1fr;
}
.archive-viz-div-row {
grid-template-columns: minmax(3.5em, 5em) 1fr auto;
gap: 4px;
}
}
.archive-calendar-section {
margin-bottom: 10px;
}
+165 -41
View File
@@ -661,56 +661,163 @@
if (!series.length) {
return '<p class="archive-viz-empty muted">当前区间暂无平仓数据</p>';
}
const w = 360;
const h = 88;
const padX = 12;
const padY = 10;
const w = 400;
const h = 110;
const padL = 36;
const padR = 8;
const padT = 8;
const padB = 22;
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 innerW = w - padL - padR;
const innerH = h - padT - padB;
const yOf = function (v) {
return padT + innerH - ((v - minV) / range) * innerH;
};
const zeroY = yOf(0);
const pts = series.map(function (s, i) {
const x = padL + (i / Math.max(series.length - 1, 1)) * innerW;
const y = yOf(s.cum);
return { x: x, y: y };
});
const linePts = pts.map(function (p) { return p.x.toFixed(1) + "," + p.y.toFixed(1); }).join(" ");
const areaPts =
linePts +
" " +
pts[pts.length - 1].x.toFixed(1) +
"," +
zeroY.toFixed(1) +
" " +
pts[0].x.toFixed(1) +
"," +
zeroY.toFixed(1);
const last = series[series.length - 1];
const lastCls = last.cum >= 0 ? "pnl-pos" : "pnl-neg";
const lineCls = last.cum >= 0 ? "archive-cum-line--up" : "archive-cum-line--down";
const sign = last.cum > 0 ? "+" : "";
const fmtAxis = function (v) {
const a = Math.abs(v);
if (a >= 100) return (v > 0 ? "+" : "") + v.toFixed(0);
if (a >= 10) return (v > 0 ? "+" : "") + v.toFixed(1);
return (v > 0 ? "+" : "") + v.toFixed(2);
};
const yTicks = [maxV, 0, minV].filter(function (v, i, arr) {
return arr.indexOf(v) === i;
});
const yLabels = yTicks
.map(function (v) {
return (
'<text x="' +
(padL - 4) +
'" y="' +
(yOf(v) + 3.5).toFixed(1) +
'" class="archive-cum-ylabel" text-anchor="end">' +
esc(fmtAxis(v)) +
"</text>"
);
})
.join("");
const xLabels =
'<text x="' +
padL +
'" y="' +
(h - 4) +
'" class="archive-cum-xlabel" text-anchor="start">' +
esc(series[0].day.slice(5)) +
"</text>" +
(series.length > 1
? '<text x="' +
(padL + innerW) +
'" y="' +
(h - 4) +
'" class="archive-cum-xlabel" text-anchor="end">' +
esc(series[series.length - 1].day.slice(5)) +
"</text>"
: "");
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" />' +
yLabels +
xLabels +
'<line x1="' + padL + '" y1="' + zeroY.toFixed(1) + '" x2="' + (w - padR) + '" y2="' + zeroY.toFixed(1) + '" class="archive-cum-zero" />' +
'<polygon points="' + areaPts + '" class="archive-cum-area ' + lineCls + '" />' +
'<polyline points="' + linePts + '" class="archive-cum-line ' + lineCls + '" />' +
'<circle cx="' + pts[pts.length - 1].x.toFixed(1) + '" cy="' + pts[pts.length - 1].y.toFixed(1) + '" r="3" class="archive-cum-dot ' + lineCls + '" />' +
"</svg>"
);
}
function barRow(label, valueLabel, pct, fillCls) {
const w = Math.max(0, Math.min(100, pct));
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="k" title="' + esc(label) + '">' + esc(label) + "</span>" +
'<div class="archive-viz-bar-track"><div class="archive-viz-bar-fill ' + fillCls + '" style="width:' + w.toFixed(1) + '%"></div></div>' +
'<span class="v">' + esc(valueLabel) + "</span>" +
"</div>"
);
}
function stackedPnlBar(profit, loss) {
const total = profit + loss;
if (total <= 0) {
return '<p class="archive-viz-empty muted">暂无盈亏数据</p>';
}
const profitPct = (profit / total) * 100;
const lossPct = 100 - profitPct;
const net = profit - loss;
const netCls = net >= 0 ? "pnl-pos" : "pnl-neg";
const netSign = net > 0 ? "+" : "";
return (
'<div class="archive-viz-stacked">' +
'<div class="archive-viz-stacked-seg archive-viz-stacked-seg--profit" style="width:' + profitPct.toFixed(1) + '%" title="总盈利 ' + profit.toFixed(2) + 'U">' +
(profitPct >= 18 ? profit.toFixed(2) + "U" : "") +
"</div>" +
'<div class="archive-viz-stacked-seg archive-viz-stacked-seg--loss" style="width:' + lossPct.toFixed(1) + '%" title="总亏损 ' + loss.toFixed(2) + 'U">' +
(lossPct >= 18 ? loss.toFixed(2) + "U" : "") +
"</div>" +
"</div>" +
'<div class="archive-viz-stacked-meta">' +
'<span class="archive-viz-legend archive-viz-legend--profit">盈利 ' + profit.toFixed(2) + "U</span>" +
'<span class="archive-viz-legend archive-viz-legend--loss">亏损 ' + loss.toFixed(2) + "U</span>" +
'<span class="archive-viz-legend archive-viz-legend--net ' + netCls + '">净 ' + netSign + net.toFixed(2) + "U</span>" +
"</div>"
);
}
function divergingBarRow(label, pnl, maxAbs) {
const absPct = (Math.abs(pnl) / maxAbs) * 50;
const cls = pnl >= 0 ? "archive-viz-div-fill--profit" : "archive-viz-div-fill--loss";
const vCls = pnl >= 0 ? "pnl-pos" : "pnl-neg";
const sign = pnl > 0 ? "+" : "";
const style =
pnl >= 0
? "left:50%;width:" + absPct.toFixed(1) + "%"
: "right:50%;width:" + absPct.toFixed(1) + "%";
return (
'<div class="archive-viz-div-row">' +
'<span class="k" title="' + esc(label) + '">' + esc(label) + "</span>" +
'<div class="archive-viz-div-track"><div class="archive-viz-div-mid"></div><div class="archive-viz-div-fill ' + cls + '" style="' + style + '"></div></div>' +
'<span class="v ' + vCls + '">' + sign + pnl.toFixed(2) + "U</span>" +
"</div>"
);
}
function renderStatsCharts() {
if (!elStatsCharts) return;
const st = dailyStats || { open_count: 0, by_exchange: {} };
const openN = st.open_count || 0;
const winN = st.win_count || 0;
const lossN = st.loss_count || 0;
const winRate = openN ? Number(st.win_rate) || 0 : 0;
const sides = sumTradePnlSides(dailyTrades);
const pnlTotal = sides.profit + sides.loss;
const netPnl = Number(st.pnl_total) || 0;
const sickN = st.sick_count || 0;
const sickPct = openN ? (sickN / openN) * 100 : 0;
const winHold = avgHoldMinutes(dailyTrades, "win");
@@ -720,13 +827,20 @@
const exKeys = Object.keys(byEx).sort();
if (elStatsVizSub) {
const pnl = Number(st.pnl_total) || 0;
const sign = pnl > 0 ? "+" : "";
const sign = netPnl > 0 ? "+" : "";
elStatsVizSub.textContent = openN
? "胜率 " + winRate.toFixed(0) + "% · " + sign + pnl.toFixed(2) + "U"
? "胜率 " + winRate.toFixed(0) + "% · " + sign + netPnl.toFixed(2) + "U"
: "暂无平仓";
}
if (!openN) {
elStatsCharts.innerHTML = '<p class="archive-viz-empty muted">当前区间暂无平仓数据</p>';
return;
}
const netCls = netPnl >= 0 ? "pnl-pos" : "pnl-neg";
const netSign = netPnl > 0 ? "+" : "";
let exBars = "";
if (exKeys.length) {
const maxAbs = Math.max.apply(
@@ -738,44 +852,54 @@
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);
return divergingBarRow(exchangeLabel(ex), pnl, maxAbs);
})
.join("");
} else {
exBars = '<p class="archive-viz-empty muted">暂无分交易所数据</p>';
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-kpis">' +
'<div class="archive-viz-kpi archive-viz-kpi--pnl">' +
'<span class="archive-viz-kpi-val ' + netCls + '">' + netSign + netPnl.toFixed(2) + "U</span>" +
'<span class="archive-viz-kpi-lbl">净盈亏</span>' +
"</div>" +
'<div class="archive-viz-kpi archive-viz-kpi--win">' +
'<div class="archive-viz-ring" style="--win-pct:' + winRate.toFixed(1) + '">' +
'<span class="archive-viz-ring-label">' + (openN ? winRate.toFixed(0) + "%" : "—") + "</span>" +
'<span class="archive-viz-ring-label">' + winRate.toFixed(0) + "%</span>" +
"</div>" +
'<span class="archive-viz-caption">胜率</span>' +
'<span class="archive-viz-kpi-lbl">' + winN + "胜 " + lossN + "负</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 class="archive-viz-kpi archive-viz-kpi--sick">' +
'<span class="archive-viz-kpi-val">' + sickN + " 笔</span>" +
'<span class="archive-viz-kpi-lbl">犯病 " + sickPct.toFixed(0) + "%</span>" +
"</div>" +
"</div>" +
'<div class="archive-viz-block">' +
'<div class="archive-viz-block-title">盈亏构成</div>' +
(pnlTotal > 0 ? stackedPnlBar(sides.profit, sides.loss) : '<p class="archive-viz-empty muted">暂无盈亏数据</p>') +
"</div>" +
'<div class="archive-viz-block archive-viz-block--exchange">' +
'<div class="archive-viz-block-title">分交易所盈亏</div>' +
'<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-hold-grid">' +
'<div class="archive-viz-hold-card archive-viz-hold-card--win">' +
'<span class="archive-viz-hold-val">' + esc(fmtDurationMinutes(winHold)) + "</span>" +
'<span class="archive-viz-hold-lbl">盈单均持仓</span>' +
'<div class="archive-viz-bar-track"><div class="archive-viz-bar-fill archive-viz-bar-fill--profit" style="width:' + (((winHold || 0) / holdMax) * 100).toFixed(1) + '%"></div></div>' +
"</div>" +
'<div class="archive-viz-hold-card archive-viz-hold-card--loss">' +
'<span class="archive-viz-hold-val">' + esc(fmtDurationMinutes(lossHold)) + "</span>" +
'<span class="archive-viz-hold-lbl">亏单均持仓</span>' +
'<div class="archive-viz-bar-track"><div class="archive-viz-bar-fill archive-viz-bar-fill--loss" style="width:' + (((lossHold || 0) / holdMax) * 100).toFixed(1) + '%"></div></div>' +
"</div>" +
"</div></div>"
: "") +
'<div class="archive-viz-block archive-viz-block--cum">' +
renderCumulativeChart(cumSeries) +
+1 -1
View File
@@ -1229,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=20260710-archive-viz"></script>
<script src="/assets/archive.js?v=20260710-archive-viz2"></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>