Add options stats holding time metrics and lightweight charts.
Show average hold duration for wins and losses, open positions, and CSS ring/bar visualizations in the stats tab. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -692,10 +692,16 @@
|
||||
const closedEl = document.getElementById("opt-stats-closed");
|
||||
const profitEl = document.getElementById("opt-stats-profit");
|
||||
const lossEl = document.getElementById("opt-stats-loss");
|
||||
const avgHoldEl = document.getElementById("opt-stats-avg-hold");
|
||||
const winHoldEl = document.getElementById("opt-stats-win-hold");
|
||||
const lossHoldEl = document.getElementById("opt-stats-loss-hold");
|
||||
const openHoldEl = document.getElementById("opt-stats-open-hold");
|
||||
const statEls = [winEl, plrEl, closedEl, profitEl, lossEl, avgHoldEl, winHoldEl, lossHoldEl, openHoldEl];
|
||||
if (!d.ok) {
|
||||
[winEl, plrEl, closedEl, profitEl, lossEl].forEach(function (el) {
|
||||
statEls.forEach(function (el) {
|
||||
if (el) el.textContent = "—";
|
||||
});
|
||||
paintStatsCharts(null);
|
||||
return;
|
||||
}
|
||||
if (winEl) winEl.textContent = d.total_closed ? d.win_rate + "%" : "0%";
|
||||
@@ -711,6 +717,96 @@
|
||||
lossEl.textContent = d.total_loss != null && d.total_loss > 0
|
||||
? fmt(d.total_loss, 2) + " USDC" : (d.total_closed ? "0 USDC" : "—");
|
||||
}
|
||||
if (avgHoldEl) avgHoldEl.textContent = fmtDuration(d.avg_hold_sec);
|
||||
if (winHoldEl) winHoldEl.textContent = fmtDuration(d.avg_win_hold_sec);
|
||||
if (lossHoldEl) lossHoldEl.textContent = fmtDuration(d.avg_loss_hold_sec);
|
||||
if (openHoldEl) {
|
||||
const cnt = Number(d.open_count) || 0;
|
||||
if (!cnt) {
|
||||
openHoldEl.textContent = "0 笔";
|
||||
} else {
|
||||
openHoldEl.textContent = cnt + " 笔 · " + fmtDuration(d.avg_open_hold_sec);
|
||||
}
|
||||
}
|
||||
paintStatsCharts(d);
|
||||
}
|
||||
|
||||
function fmtDuration(sec) {
|
||||
if (sec == null || sec === "" || Number.isNaN(Number(sec))) return "—";
|
||||
let s = Math.max(0, Math.round(Number(sec)));
|
||||
if (s < 60) return s + "秒";
|
||||
const m = Math.floor(s / 60);
|
||||
if (m < 60) {
|
||||
const rs = s % 60;
|
||||
return rs ? m + "分" + rs + "秒" : 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 setBarFill(el, pct) {
|
||||
if (!el) return;
|
||||
const n = Math.max(0, Math.min(100, Number(pct) || 0));
|
||||
el.style.width = n + "%";
|
||||
}
|
||||
|
||||
function paintStatsCharts(d) {
|
||||
const ring = document.getElementById("opt-stats-ring");
|
||||
const ringLabel = document.getElementById("opt-stats-ring-label");
|
||||
const profitBar = document.getElementById("opt-stats-bar-profit");
|
||||
const lossBar = document.getElementById("opt-stats-bar-loss");
|
||||
const profitBarLabel = document.getElementById("opt-stats-bar-profit-label");
|
||||
const lossBarLabel = document.getElementById("opt-stats-bar-loss-label");
|
||||
const winHoldBar = document.getElementById("opt-stats-bar-win-hold");
|
||||
const lossHoldBar = document.getElementById("opt-stats-bar-loss-hold");
|
||||
const winHoldBarLabel = document.getElementById("opt-stats-win-hold-label");
|
||||
const lossHoldBarLabel = document.getElementById("opt-stats-loss-hold-label");
|
||||
if (!d || !d.ok) {
|
||||
if (ring) ring.style.setProperty("--win-pct", "0");
|
||||
if (ringLabel) ringLabel.textContent = "—";
|
||||
[profitBar, lossBar, winHoldBar, lossHoldBar].forEach(function (el) { setBarFill(el, 0); });
|
||||
[profitBarLabel, lossBarLabel, winHoldBarLabel, lossHoldBarLabel].forEach(function (el) {
|
||||
if (el) el.textContent = "—";
|
||||
});
|
||||
return;
|
||||
}
|
||||
const winRate = d.total_closed ? Number(d.win_rate) || 0 : 0;
|
||||
if (ring) ring.style.setProperty("--win-pct", String(winRate));
|
||||
if (ringLabel) ringLabel.textContent = d.total_closed ? winRate.toFixed(0) + "%" : "0%";
|
||||
|
||||
const profit = Math.max(0, Number(d.total_profit) || 0);
|
||||
const loss = Math.max(0, Number(d.total_loss) || 0);
|
||||
const pnlTotal = profit + loss;
|
||||
if (pnlTotal > 0) {
|
||||
setBarFill(profitBar, (profit / pnlTotal) * 100);
|
||||
setBarFill(lossBar, (loss / pnlTotal) * 100);
|
||||
if (profitBarLabel) profitBarLabel.textContent = fmt(profit, 2) + " USDC";
|
||||
if (lossBarLabel) lossBarLabel.textContent = fmt(loss, 2) + " USDC";
|
||||
} else {
|
||||
setBarFill(profitBar, 0);
|
||||
setBarFill(lossBar, 0);
|
||||
if (profitBarLabel) profitBarLabel.textContent = d.total_closed ? "0 USDC" : "—";
|
||||
if (lossBarLabel) lossBarLabel.textContent = d.total_closed ? "0 USDC" : "—";
|
||||
}
|
||||
|
||||
const winHold = Number(d.avg_win_hold_sec) || 0;
|
||||
const lossHold = Number(d.avg_loss_hold_sec) || 0;
|
||||
const holdMax = Math.max(winHold, lossHold);
|
||||
if (holdMax > 0) {
|
||||
setBarFill(winHoldBar, (winHold / holdMax) * 100);
|
||||
setBarFill(lossHoldBar, (lossHold / holdMax) * 100);
|
||||
if (winHoldBarLabel) winHoldBarLabel.textContent = fmtDuration(d.avg_win_hold_sec);
|
||||
if (lossHoldBarLabel) lossHoldBarLabel.textContent = fmtDuration(d.avg_loss_hold_sec);
|
||||
} else {
|
||||
setBarFill(winHoldBar, 0);
|
||||
setBarFill(lossHoldBar, 0);
|
||||
if (winHoldBarLabel) winHoldBarLabel.textContent = "—";
|
||||
if (lossHoldBarLabel) lossHoldBarLabel.textContent = "—";
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteHistoryRow(id, status) {
|
||||
|
||||
Reference in New Issue
Block a user