feat(hub): auto-mark all archive trades on chart

Add 自动 toggle on archive chart; when on, load history span for all trades and plot numbered open/close arrows for each.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-07 23:26:57 +08:00
parent 5ceacd8077
commit 69f554214c
3 changed files with 140 additions and 16 deletions
+27
View File
@@ -3972,6 +3972,9 @@ body.hub-page-ai #page-ai {
border-color: var(--accent);
background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.archive-chart-wrap {
position: relative;
}
.archive-chart-host {
height: 360px;
min-height: 280px;
@@ -3980,6 +3983,30 @@ body.hub-page-ai #page-ai {
background: var(--panel);
overflow: hidden;
}
.archive-mark-auto {
position: absolute;
right: 8px;
bottom: 10px;
z-index: 5;
padding: 4px 10px;
font-size: 0.72rem;
font-family: var(--font);
border-radius: 6px;
border: 1px solid var(--border-soft);
background: var(--chart-bar-bg, var(--inset-surface));
color: var(--muted);
cursor: pointer;
line-height: 1.2;
}
.archive-mark-auto:hover {
border-color: var(--accent);
color: var(--text);
}
.archive-mark-auto.is-on {
color: #22c55e;
border-color: rgba(34, 197, 94, 0.45);
background: rgba(34, 197, 94, 0.1);
}
.archive-trades {
overflow: auto;
max-height: 280px;
+107 -13
View File
@@ -23,7 +23,9 @@
const elBtnJump = document.getElementById("archive-btn-jump");
const elBtnReloadChart = document.getElementById("archive-btn-reload-chart");
const elChartHost = document.getElementById("archive-chart");
const elMarkAuto = document.getElementById("archive-mark-auto");
const elTrades = document.getElementById("archive-trades");
const ARCHIVE_MARK_AUTO_KEY = "hubArchiveMarkAuto";
const TF_MS = {
"5m": 5 * 60_000,
@@ -42,6 +44,41 @@
let candleSeries = null;
let volumeSeries = null;
let inited = false;
let markAuto = true;
let lastCandles = [];
function loadMarkAutoPref() {
try {
const raw = localStorage.getItem(ARCHIVE_MARK_AUTO_KEY);
if (raw === "0" || raw === "false") markAuto = false;
else if (raw === "1" || raw === "true") markAuto = true;
} catch (_) {}
syncMarkAutoBtn();
}
function syncMarkAutoBtn() {
if (!elMarkAuto) return;
elMarkAuto.classList.toggle("is-on", markAuto);
elMarkAuto.setAttribute("aria-pressed", markAuto ? "true" : "false");
}
function saveMarkAutoPref() {
try {
localStorage.setItem(ARCHIVE_MARK_AUTO_KEY, markAuto ? "1" : "0");
} catch (_) {}
}
function tradeHistoryBounds(tradeList) {
let minOpen = null;
let maxClose = null;
(tradeList || []).forEach(function (tr) {
const o = tradeOpenMs(tr);
const c = tradeCloseMs(tr);
if (o != null) minOpen = minOpen == null ? o : Math.min(minOpen, o);
if (c != null) maxClose = maxClose == null ? c : Math.max(maxClose, c);
});
return { minOpen: minOpen, maxClose: maxClose };
}
function fmt(n, d) {
if (n == null || n === "" || !Number.isFinite(Number(n))) return "—";
@@ -223,35 +260,71 @@
return d === "long" || d === "多" || d === "buy";
}
function buildTradeMarkers(tr, candles, tf) {
function buildTradeMarkers(tr, candles, tf, opts) {
if (!tr || !candles.length) return [];
const options = opts || {};
const suffix = options.labelSuffix ? String(options.labelSuffix) : "";
const highlight = !!options.highlight;
const long = isLongDirection(tr.direction);
const openMs = tradeOpenMs(tr);
const closeMs = tradeCloseMs(tr);
const openColor = highlight ? (long ? "#4ade80" : "#f87171") : long ? "#22c55e" : "#ef4444";
let closeColor = highlight ? "#fbbf24" : "#f59e0b";
const pnl = Number(tr.pnl_amount);
if (!highlight && Number.isFinite(pnl) && pnl < -0.0001) {
closeColor = "#a855f7";
}
const markers = [];
if (openMs) {
markers.push({
time: snapToCandleTime(msToBarTime(openMs, tf), candles),
position: long ? "belowBar" : "aboveBar",
color: long ? "#22c55e" : "#ef4444",
color: openColor,
shape: long ? "arrowUp" : "arrowDown",
text: "开",
text: "开" + suffix,
});
}
if (closeMs) {
markers.push({
time: snapToCandleTime(msToBarTime(closeMs, tf), candles),
position: long ? "aboveBar" : "belowBar",
color: "#f59e0b",
color: closeColor,
shape: long ? "arrowDown" : "arrowUp",
text: "平",
text: "平" + suffix,
});
}
return markers.sort(function (a, b) {
return markers;
}
function buildChartMarkers(candles, tf) {
if (!candles.length) return [];
const tr = pickAnchorTrade();
if (!markAuto || !trades.length) {
return buildTradeMarkers(tr, candles, tf, { highlight: true });
}
const sorted = trades.slice().sort(function (a, b) {
return (tradeOpenMs(a) || 0) - (tradeOpenMs(b) || 0);
});
const multi = sorted.length > 1;
const out = [];
sorted.forEach(function (row, idx) {
const tid = String(row.trade_id || row.id);
const parts = buildTradeMarkers(row, candles, tf, {
labelSuffix: multi ? String(idx + 1) : "",
highlight: tid === String(selectedTradeId),
});
out.push.apply(out, parts);
});
return out.sort(function (a, b) {
return a.time > b.time ? 1 : a.time < b.time ? -1 : 0;
});
}
function applyChartMarkers() {
if (!candleSeries || !candleSeries.setMarkers || !lastCandles.length) return;
candleSeries.setMarkers(buildChartMarkers(lastCandles, timeframe));
}
/** 初始只聚焦持仓段;完整历史已加载,可向左拖动/滚轮缩小查看建仓前全局。 */
function focusInitialTradeView(candles, tr, tf) {
if (!chart || !candles.length || !tr) return;
@@ -362,8 +435,16 @@
const tr = pickAnchorTrade();
const anchor = anchorMsForTrade(tr);
const jump = (elJumpAt && elJumpAt.value || "").trim();
const openMs = tradeOpenMs(tr);
const closeMs = tradeCloseMs(tr);
let openMs = null;
let closeMs = null;
if (markAuto && trades.length) {
const bounds = tradeHistoryBounds(trades);
openMs = bounds.minOpen;
closeMs = bounds.maxClose;
} else if (tr) {
openMs = tradeOpenMs(tr);
closeMs = tradeCloseMs(tr);
}
const params = new URLSearchParams({
exchange_key: selected.exchange_key,
symbol: selected.symbol,
@@ -388,6 +469,7 @@
}
ensureChart();
const candles = j.candles || [];
lastCandles = candles;
candleSeries.setData(
candles.map(function (c) {
return { time: c.time, open: c.open, high: c.high, low: c.low, close: c.close };
@@ -402,16 +484,15 @@
};
})
);
if (candleSeries.setMarkers) {
candleSeries.setMarkers(buildTradeMarkers(tr, candles, timeframe));
}
applyChartMarkers();
if (tr && tradeOpenMs(tr) && tradeCloseMs(tr)) {
focusInitialTradeView(candles, tr, timeframe);
} else if (candles.length > 10) {
chart.timeScale().setVisibleLogicalRange({ from: candles.length - 120, to: candles.length + 5 });
}
const markHint = markAuto && trades.length > 1 ? " · 自动标注 " + trades.length + " 笔" : tr ? " · 已标注开/平" : "";
const histHint = openMs && closeMs ? " · 可拖动/滚轮缩放查看建仓前走势" : "";
setStatus("K 线 " + candles.length + " 根 · " + timeframe + (tr ? " · 已标注开/平" : "") + histHint);
setStatus("K 线 " + candles.length + " 根 · " + timeframe + markHint + histHint);
}
function renderTrades() {
@@ -478,7 +559,11 @@
if (ev.target.closest("select") || ev.target.closest("input")) return;
selectedTradeId = row.getAttribute("data-id");
renderTrades();
loadChart();
applyChartMarkers();
const trSel = pickAnchorTrade();
if (trSel && tradeOpenMs(trSel) && tradeCloseMs(trSel)) {
focusInitialTradeView(lastCandles, trSel, timeframe);
}
});
});
elTrades.querySelectorAll(".archive-tag-select").forEach(function (sel) {
@@ -629,6 +714,14 @@
}
if (elViewMode) elViewMode.addEventListener("change", loadChart);
if (elBtnReloadChart) elBtnReloadChart.addEventListener("click", loadChart);
if (elMarkAuto) {
elMarkAuto.addEventListener("click", function () {
markAuto = !markAuto;
syncMarkAutoBtn();
saveMarkAutoPref();
loadChart();
});
}
if (elBtnJump) {
elBtnJump.addEventListener("click", function () {
loadChart();
@@ -641,6 +734,7 @@
return;
}
if (!inited) {
loadMarkAutoPref();
bindEvents();
inited = true;
}
+6 -3
View File
@@ -15,7 +15,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
<link rel="stylesheet" href="/assets/app.css?v=20260607-hub-archive-v1" />
<link rel="stylesheet" href="/assets/app.css?v=20260607-hub-archive-v5" />
</head>
<body>
<div class="app-bg" aria-hidden="true"></div>
@@ -234,7 +234,10 @@
<button type="button" id="archive-btn-jump" class="ghost">跳转</button>
<button type="button" id="archive-btn-reload-chart" class="primary">重载图表</button>
</div>
<div id="archive-chart" class="archive-chart-host"></div>
<div class="archive-chart-wrap">
<div id="archive-chart" class="archive-chart-host"></div>
<button type="button" id="archive-mark-auto" class="archive-mark-auto is-on" title="开启:该币种全部交易均标注开/平;关闭:仅当前选中一笔">自动</button>
</div>
<div id="archive-trades" class="archive-trades"></div>
</section>
</div>
@@ -349,7 +352,7 @@
<div id="toast"></div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="/assets/chart.js?v=20260604-upnl-contracts"></script>
<script src="/assets/archive.js?v=20260607-hub-archive-v4"></script>
<script src="/assets/archive.js?v=20260607-hub-archive-v5"></script>
<script src="/assets/ai_review_render.js?v=2"></script>
<script src="/assets/app.js?v=20260607-hub-archive-v1"></script>
</body>