From daf023b403e91a5c2ef2ef1f90aeb908e5793c20 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 10 Jul 2026 11:58:27 +0800 Subject: [PATCH] Improve archive stats chart layout and fix invisible bar colors. Co-authored-by: Cursor --- manual_trading_hub/static/app.css | 246 +++++++++++++++++++++++---- manual_trading_hub/static/archive.js | 206 +++++++++++++++++----- manual_trading_hub/static/index.html | 2 +- 3 files changed, 376 insertions(+), 78 deletions(-) diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 2eb49cc..f0f866e 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -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; } diff --git a/manual_trading_hub/static/archive.js b/manual_trading_hub/static/archive.js index 20e1087..a750f89 100644 --- a/manual_trading_hub/static/archive.js +++ b/manual_trading_hub/static/archive.js @@ -661,56 +661,163 @@ if (!series.length) { return '

当前区间暂无平仓数据

'; } - 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 ( + '' + + esc(fmtAxis(v)) + + "" + ); + }) + .join(""); + const xLabels = + '' + + esc(series[0].day.slice(5)) + + "" + + (series.length > 1 + ? '' + + esc(series[series.length - 1].day.slice(5)) + + "" + : ""); return ( '
' + '累计盈亏' + '' + sign + last.cum.toFixed(2) + "U" + "
" + '" ); } function barRow(label, valueLabel, pct, fillCls) { + const w = Math.max(0, Math.min(100, pct)); return ( '
' + - '' + esc(label) + "" + - '
' + + '' + esc(label) + "" + + '
' + '' + esc(valueLabel) + "" + "
" ); } + function stackedPnlBar(profit, loss) { + const total = profit + loss; + if (total <= 0) { + return '

暂无盈亏数据

'; + } + 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 ( + '
' + + '
' + + (profitPct >= 18 ? profit.toFixed(2) + "U" : "") + + "
" + + '
' + + (lossPct >= 18 ? loss.toFixed(2) + "U" : "") + + "
" + + "
" + + '
' + + '盈利 ' + profit.toFixed(2) + "U" + + '亏损 ' + loss.toFixed(2) + "U" + + '净 ' + netSign + net.toFixed(2) + "U" + + "
" + ); + } + + 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 ( + '
' + + '' + esc(label) + "" + + '
' + + '' + sign + pnl.toFixed(2) + "U" + + "
" + ); + } + 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 = '

当前区间暂无平仓数据

'; + 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 = '

暂无分交易所数据

'; + exBars = '

暂无分策略数据

'; } const cumSeries = buildCumulativeSeries(dailyTrades); elStatsCharts.innerHTML = - '
' + - '
' + + '
' + + '
' + + '' + netSign + netPnl.toFixed(2) + "U" + + '净盈亏' + + "
" + + '
' + '
' + - '' + (openN ? winRate.toFixed(0) + "%" : "—") + "" + + '' + winRate.toFixed(0) + "%" + "
" + - '胜率' + + '' + winN + "胜 " + lossN + "负" + "
" + - '
' + - (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") + + '
' + + '' + sickN + " 笔" + + '犯病 " + sickPct.toFixed(0) + "%" + "
" + "
" + + '
' + + '
盈亏构成
' + + (pnlTotal > 0 ? stackedPnlBar(sides.profit, sides.loss) : '

暂无盈亏数据

') + + "
" + '
' + - '
分交易所盈亏
' + + '
分策略盈亏
' + exBars + "
" + (holdMax > 0 ? '
' + '
持仓时长对比
' + - barRow("盈单", fmtDurationMinutes(winHold), ((winHold || 0) / holdMax) * 100, "archive-viz-bar-fill--profit") + - barRow("亏单", fmtDurationMinutes(lossHold), ((lossHold || 0) / holdMax) * 100, "archive-viz-bar-fill--loss") + - "
" + '
' + + '
' + + '' + esc(fmtDurationMinutes(winHold)) + "" + + '盈单均持仓' + + '
' + + "
" + + '
' + + '' + esc(fmtDurationMinutes(lossHold)) + "" + + '亏单均持仓' + + '
' + + "
" + + "
" : "") + '
' + renderCumulativeChart(cumSeries) + diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 75b463a..a78ff57 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -1229,7 +1229,7 @@ - +