feat: market divergence scan tabs (4h/1d/1w) with Top20 MACD confluence colors

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-06 19:58:30 +08:00
parent 81f7a7c451
commit e62a3f3351
7 changed files with 999 additions and 48 deletions
+137 -26
View File
@@ -154,11 +154,11 @@
const elSymbol = document.getElementById("market-symbol");
const elVolRankMeta = document.getElementById("market-vol-rank-meta");
const elVolRankList = document.getElementById("market-vol-rank-list");
const elVolRankBtn = document.getElementById("market-vol-rank-btn");
const elFsVolRankBtn = document.getElementById("market-fs-vol-rank-btn");
const elVolRankSheet = document.getElementById("market-vol-rank-sheet");
const elVolRankAnchor = document.getElementById("market-vol-rank-anchor");
const elVolRankAnchorFs = document.getElementById("market-vol-rank-anchor-fs");
let activeScanTab = "top20";
let scanSheetOpen = false;
const elTf = document.getElementById("market-timeframe");
const elRefresh = document.getElementById("market-refresh");
const elStatus = document.getElementById("market-status");
@@ -2965,6 +2965,19 @@
void postChartUnwatch();
}
function allScanTabButtons() {
return Array.prototype.slice.call(document.querySelectorAll(".market-scan-tab"));
}
function setActiveScanTab(tab) {
activeScanTab = tab || "top20";
allScanTabButtons().forEach(function (btn) {
const on = btn.getAttribute("data-scan-tab") === activeScanTab;
btn.classList.toggle("is-active", on);
btn.setAttribute("aria-selected", on ? "true" : "false");
});
}
function mountVolRankSheet(forFullscreen) {
if (!elVolRankSheet) return;
const anchor = forFullscreen ? elVolRankAnchorFs : elVolRankAnchor;
@@ -2972,40 +2985,142 @@
anchor.appendChild(elVolRankSheet);
}
function setVolRankBtnActive(btn, on) {
if (!btn) return;
btn.classList.toggle("is-active", on);
btn.setAttribute("aria-expanded", on ? "true" : "false");
}
function setVolRankSheetOpen(open) {
function setScanSheetOpen(open, tab) {
const on = !!open;
scanSheetOpen = on;
if (tab) setActiveScanTab(tab);
if (elVolRankSheet) {
elVolRankSheet.classList.toggle("hidden", !on);
elVolRankSheet.setAttribute("aria-hidden", on ? "false" : "true");
}
setVolRankBtnActive(elVolRankBtn, on);
setVolRankBtnActive(elFsVolRankBtn, on);
if (on) void loadVolumeRank();
if (on) void loadScanPanel(false);
}
function loadScanPanel(forceRefresh) {
if (activeScanTab === "top20") {
void loadVolumeRank(forceRefresh);
return;
}
void loadDivergenceScan(activeScanTab, forceRefresh);
}
function bindVolRankPanel() {
function toggleVolRankSheet() {
const open = elVolRankSheet && elVolRankSheet.classList.contains("hidden");
setVolRankSheetOpen(open);
}
if (elVolRankBtn) elVolRankBtn.addEventListener("click", toggleVolRankSheet);
if (elFsVolRankBtn) elFsVolRankBtn.addEventListener("click", toggleVolRankSheet);
allScanTabButtons().forEach(function (btn) {
btn.addEventListener("click", function () {
const tab = btn.getAttribute("data-scan-tab") || "top20";
if (scanSheetOpen && activeScanTab === tab) {
setScanSheetOpen(false);
return;
}
setScanSheetOpen(true, tab);
});
});
document.addEventListener("pointerdown", function (ev) {
if (!elVolRankSheet || elVolRankSheet.classList.contains("hidden")) return;
const t = ev.target;
if (elVolRankSheet.contains(t)) return;
if (elVolRankBtn && elVolRankBtn.contains(t)) return;
if (elFsVolRankBtn && elFsVolRankBtn.contains(t)) return;
setVolRankSheetOpen(false);
if (t && t.closest && t.closest(".market-scan-tabs")) return;
setScanSheetOpen(false);
});
}
function applyScanSymbolSelection(symbol, tabTf) {
if (!symbol) return;
if (elSymbol) elSymbol.value = symbol;
if (elFsSymbol) elFsSymbol.value = symbol;
if (tabTf && tabTf !== "top20") {
if (elTf) elTf.value = tabTf;
if (elFsTf) elFsTf.value = tabTf;
if (elIndMacd) elIndMacd.checked = true;
indicatorState.macd = true;
}
setScanSheetOpen(false);
loadChart(false);
}
function renderDivergenceScan(data) {
if (!elVolRankMeta || !elVolRankList) return;
elVolRankList.innerHTML = "";
const tabLabel = (data && data.tab_label) || "背离";
if (!data || !data.ok || !data.items || !data.items.length) {
elVolRankMeta.textContent =
(data && data.msg) ||
tabLabel + "Top20 内暂无 MACD 背离(可点「清库重拉」后重试扫描)";
return;
}
const rankDate = data.rank_date || "—";
const updated = data.scanned_at || data.updated_at || "—";
let meta =
tabLabel +
" · Top20 内 MACD 档A · 交易日 " +
rankDate +
" · 扫描 " +
updated +
" · " +
data.items.length +
" 条";
elVolRankMeta.textContent = meta;
const curSym = (elSymbol && elSymbol.value.trim().toUpperCase()) || "";
const tabTf = data.tab || activeScanTab;
data.items.forEach(function (row) {
const li = document.createElement("li");
const btn = document.createElement("button");
btn.type = "button";
const css = row.confluence_css || "none";
btn.className = "market-vol-rank-item is-div-scan confluence-" + css;
if (row.symbol && row.symbol.toUpperCase() === curSym) {
btn.classList.add("is-active");
}
btn.dataset.symbol = row.symbol || "";
const dirLabel = row.is_split
? "分歧"
: row.tab_direction_label || row.direction_label || "";
const confLabel = row.is_split ? row.split_detail || "分歧" : row.confluence_kind || "";
const fresh = row.tab_freshness || "";
btn.innerHTML =
'<span class="market-vol-rank-badge confluence-' +
css +
'">' +
(confLabel || "—") +
'</span><span class="market-vol-rank-no">' +
(row.rank || "") +
'</span><span class="market-vol-rank-sym">' +
(row.symbol || "") +
'</span><span class="market-vol-rank-div">' +
dirLabel +
(fresh ? " · " + fresh : "") +
"</span>";
btn.addEventListener("click", function () {
applyScanSymbolSelection(row.symbol, tabTf);
});
li.appendChild(btn);
elVolRankList.appendChild(li);
});
}
async function loadDivergenceScan(tab, forceRefresh) {
const exKey = (elExchange && elExchange.value) || "";
if (!exKey || !elVolRankMeta) return;
elVolRankMeta.textContent = "扫描背离…";
if (elVolRankList) elVolRankList.innerHTML = "";
try {
let url =
"/api/chart/divergence-scan?exchange_key=" +
encodeURIComponent(exKey) +
"&tab=" +
encodeURIComponent(tab || "4h");
if (forceRefresh) url += "&refresh=1";
const r = await fetch(url, { credentials: "same-origin" });
const data = await r.json();
if (!r.ok) {
throw new Error((data && data.detail) || (data && data.msg) || "加载失败");
}
renderDivergenceScan(data);
} catch (e) {
renderDivergenceScan({ ok: false, msg: String(e.message || e), tab_label: tab });
}
}
function renderVolumeRank(data) {
if (!elVolRankMeta || !elVolRankList) return;
elVolRankList.innerHTML = "";
@@ -3056,11 +3171,7 @@
(row.volume_label || "") +
"</span>";
btn.addEventListener("click", function () {
if (!row.symbol) return;
if (elSymbol) elSymbol.value = row.symbol;
if (elFsSymbol) elFsSymbol.value = row.symbol;
setVolRankSheetOpen(false);
loadChart(false);
applyScanSymbolSelection(row.symbol, "top20");
});
li.appendChild(btn);
elVolRankList.appendChild(li);