diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index dac8dfd..2eb49cc 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -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; } diff --git a/manual_trading_hub/static/archive.js b/manual_trading_hub/static/archive.js index 9c02588..20e1087 100644 --- a/manual_trading_hub/static/archive.js +++ b/manual_trading_hub/static/archive.js @@ -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 @@ "
" + rows + ""; + 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 '当前区间暂无平仓数据
'; + } + 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 ( + '暂无分交易所数据
'; + } + + const cumSeries = buildCumulativeSeries(dailyTrades); + elStatsCharts.innerHTML = + '