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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 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>
|
||||
|
||||
Reference in New Issue
Block a user