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);
|
border-color: var(--accent);
|
||||||
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||||
}
|
}
|
||||||
|
.archive-chart-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
.archive-chart-host {
|
.archive-chart-host {
|
||||||
height: 360px;
|
height: 360px;
|
||||||
min-height: 280px;
|
min-height: 280px;
|
||||||
@@ -3980,6 +3983,30 @@ body.hub-page-ai #page-ai {
|
|||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
overflow: hidden;
|
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 {
|
.archive-trades {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
max-height: 280px;
|
max-height: 280px;
|
||||||
|
|||||||
@@ -23,7 +23,9 @@
|
|||||||
const elBtnJump = document.getElementById("archive-btn-jump");
|
const elBtnJump = document.getElementById("archive-btn-jump");
|
||||||
const elBtnReloadChart = document.getElementById("archive-btn-reload-chart");
|
const elBtnReloadChart = document.getElementById("archive-btn-reload-chart");
|
||||||
const elChartHost = document.getElementById("archive-chart");
|
const elChartHost = document.getElementById("archive-chart");
|
||||||
|
const elMarkAuto = document.getElementById("archive-mark-auto");
|
||||||
const elTrades = document.getElementById("archive-trades");
|
const elTrades = document.getElementById("archive-trades");
|
||||||
|
const ARCHIVE_MARK_AUTO_KEY = "hubArchiveMarkAuto";
|
||||||
|
|
||||||
const TF_MS = {
|
const TF_MS = {
|
||||||
"5m": 5 * 60_000,
|
"5m": 5 * 60_000,
|
||||||
@@ -42,6 +44,41 @@
|
|||||||
let candleSeries = null;
|
let candleSeries = null;
|
||||||
let volumeSeries = null;
|
let volumeSeries = null;
|
||||||
let inited = false;
|
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) {
|
function fmt(n, d) {
|
||||||
if (n == null || n === "" || !Number.isFinite(Number(n))) return "—";
|
if (n == null || n === "" || !Number.isFinite(Number(n))) return "—";
|
||||||
@@ -223,35 +260,71 @@
|
|||||||
return d === "long" || d === "多" || d === "buy";
|
return d === "long" || d === "多" || d === "buy";
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTradeMarkers(tr, candles, tf) {
|
function buildTradeMarkers(tr, candles, tf, opts) {
|
||||||
if (!tr || !candles.length) return [];
|
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 long = isLongDirection(tr.direction);
|
||||||
const openMs = tradeOpenMs(tr);
|
const openMs = tradeOpenMs(tr);
|
||||||
const closeMs = tradeCloseMs(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 = [];
|
const markers = [];
|
||||||
if (openMs) {
|
if (openMs) {
|
||||||
markers.push({
|
markers.push({
|
||||||
time: snapToCandleTime(msToBarTime(openMs, tf), candles),
|
time: snapToCandleTime(msToBarTime(openMs, tf), candles),
|
||||||
position: long ? "belowBar" : "aboveBar",
|
position: long ? "belowBar" : "aboveBar",
|
||||||
color: long ? "#22c55e" : "#ef4444",
|
color: openColor,
|
||||||
shape: long ? "arrowUp" : "arrowDown",
|
shape: long ? "arrowUp" : "arrowDown",
|
||||||
text: "开",
|
text: "开" + suffix,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (closeMs) {
|
if (closeMs) {
|
||||||
markers.push({
|
markers.push({
|
||||||
time: snapToCandleTime(msToBarTime(closeMs, tf), candles),
|
time: snapToCandleTime(msToBarTime(closeMs, tf), candles),
|
||||||
position: long ? "aboveBar" : "belowBar",
|
position: long ? "aboveBar" : "belowBar",
|
||||||
color: "#f59e0b",
|
color: closeColor,
|
||||||
shape: long ? "arrowDown" : "arrowUp",
|
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;
|
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) {
|
function focusInitialTradeView(candles, tr, tf) {
|
||||||
if (!chart || !candles.length || !tr) return;
|
if (!chart || !candles.length || !tr) return;
|
||||||
@@ -362,8 +435,16 @@
|
|||||||
const tr = pickAnchorTrade();
|
const tr = pickAnchorTrade();
|
||||||
const anchor = anchorMsForTrade(tr);
|
const anchor = anchorMsForTrade(tr);
|
||||||
const jump = (elJumpAt && elJumpAt.value || "").trim();
|
const jump = (elJumpAt && elJumpAt.value || "").trim();
|
||||||
const openMs = tradeOpenMs(tr);
|
let openMs = null;
|
||||||
const closeMs = tradeCloseMs(tr);
|
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({
|
const params = new URLSearchParams({
|
||||||
exchange_key: selected.exchange_key,
|
exchange_key: selected.exchange_key,
|
||||||
symbol: selected.symbol,
|
symbol: selected.symbol,
|
||||||
@@ -388,6 +469,7 @@
|
|||||||
}
|
}
|
||||||
ensureChart();
|
ensureChart();
|
||||||
const candles = j.candles || [];
|
const candles = j.candles || [];
|
||||||
|
lastCandles = candles;
|
||||||
candleSeries.setData(
|
candleSeries.setData(
|
||||||
candles.map(function (c) {
|
candles.map(function (c) {
|
||||||
return { time: c.time, open: c.open, high: c.high, low: c.low, close: c.close };
|
return { time: c.time, open: c.open, high: c.high, low: c.low, close: c.close };
|
||||||
@@ -402,16 +484,15 @@
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
if (candleSeries.setMarkers) {
|
applyChartMarkers();
|
||||||
candleSeries.setMarkers(buildTradeMarkers(tr, candles, timeframe));
|
|
||||||
}
|
|
||||||
if (tr && tradeOpenMs(tr) && tradeCloseMs(tr)) {
|
if (tr && tradeOpenMs(tr) && tradeCloseMs(tr)) {
|
||||||
focusInitialTradeView(candles, tr, timeframe);
|
focusInitialTradeView(candles, tr, timeframe);
|
||||||
} else if (candles.length > 10) {
|
} else if (candles.length > 10) {
|
||||||
chart.timeScale().setVisibleLogicalRange({ from: candles.length - 120, to: candles.length + 5 });
|
chart.timeScale().setVisibleLogicalRange({ from: candles.length - 120, to: candles.length + 5 });
|
||||||
}
|
}
|
||||||
|
const markHint = markAuto && trades.length > 1 ? " · 自动标注 " + trades.length + " 笔" : tr ? " · 已标注开/平" : "";
|
||||||
const histHint = openMs && closeMs ? " · 可拖动/滚轮缩放查看建仓前走势" : "";
|
const histHint = openMs && closeMs ? " · 可拖动/滚轮缩放查看建仓前走势" : "";
|
||||||
setStatus("K 线 " + candles.length + " 根 · " + timeframe + (tr ? " · 已标注开/平" : "") + histHint);
|
setStatus("K 线 " + candles.length + " 根 · " + timeframe + markHint + histHint);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTrades() {
|
function renderTrades() {
|
||||||
@@ -478,7 +559,11 @@
|
|||||||
if (ev.target.closest("select") || ev.target.closest("input")) return;
|
if (ev.target.closest("select") || ev.target.closest("input")) return;
|
||||||
selectedTradeId = row.getAttribute("data-id");
|
selectedTradeId = row.getAttribute("data-id");
|
||||||
renderTrades();
|
renderTrades();
|
||||||
loadChart();
|
applyChartMarkers();
|
||||||
|
const trSel = pickAnchorTrade();
|
||||||
|
if (trSel && tradeOpenMs(trSel) && tradeCloseMs(trSel)) {
|
||||||
|
focusInitialTradeView(lastCandles, trSel, timeframe);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
elTrades.querySelectorAll(".archive-tag-select").forEach(function (sel) {
|
elTrades.querySelectorAll(".archive-tag-select").forEach(function (sel) {
|
||||||
@@ -629,6 +714,14 @@
|
|||||||
}
|
}
|
||||||
if (elViewMode) elViewMode.addEventListener("change", loadChart);
|
if (elViewMode) elViewMode.addEventListener("change", loadChart);
|
||||||
if (elBtnReloadChart) elBtnReloadChart.addEventListener("click", loadChart);
|
if (elBtnReloadChart) elBtnReloadChart.addEventListener("click", loadChart);
|
||||||
|
if (elMarkAuto) {
|
||||||
|
elMarkAuto.addEventListener("click", function () {
|
||||||
|
markAuto = !markAuto;
|
||||||
|
syncMarkAutoBtn();
|
||||||
|
saveMarkAutoPref();
|
||||||
|
loadChart();
|
||||||
|
});
|
||||||
|
}
|
||||||
if (elBtnJump) {
|
if (elBtnJump) {
|
||||||
elBtnJump.addEventListener("click", function () {
|
elBtnJump.addEventListener("click", function () {
|
||||||
loadChart();
|
loadChart();
|
||||||
@@ -641,6 +734,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!inited) {
|
if (!inited) {
|
||||||
|
loadMarkAutoPref();
|
||||||
bindEvents();
|
bindEvents();
|
||||||
inited = true;
|
inited = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<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'" />
|
<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>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-bg" aria-hidden="true"></div>
|
<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-jump" class="ghost">跳转</button>
|
||||||
<button type="button" id="archive-btn-reload-chart" class="primary">重载图表</button>
|
<button type="button" id="archive-btn-reload-chart" class="primary">重载图表</button>
|
||||||
</div>
|
</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>
|
<div id="archive-trades" class="archive-trades"></div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -349,7 +352,7 @@
|
|||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
<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/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/ai_review_render.js?v=2"></script>
|
||||||
<script src="/assets/app.js?v=20260607-hub-archive-v1"></script>
|
<script src="/assets/app.js?v=20260607-hub-archive-v1"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user