行情区:MACD/RSI置于成交量下方、背离标注与全屏切换品种周期

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-02 14:33:33 +08:00
parent 84ac9134db
commit 5661ccf4ab
3 changed files with 371 additions and 39 deletions
+309 -37
View File
@@ -8,6 +8,11 @@
const CANDLE_SCALE_BOTTOM = 0.26;
const VOLUME_SCALE_TOP = 0.73;
const VOLUME_SCALE_BOTTOM = 0.06;
const PANEL_VOL_H = 0.12;
const PANEL_MACD_H = 0.14;
const PANEL_RSI_H = 0.14;
const SWING_LOOKBACK = 4;
const MAX_DIV_MARKERS = 4;
const TF_MS = {
"1m": 60_000,
"5m": 5 * 60_000,
@@ -55,6 +60,12 @@
const elIndEma = document.getElementById("market-ind-ema");
const elIndMacd = document.getElementById("market-ind-macd");
const elIndRsi = document.getElementById("market-ind-rsi");
const elFsToolbar = document.getElementById("market-fs-toolbar");
const elFsExchange = document.getElementById("market-fs-exchange");
const elFsSymbol = document.getElementById("market-fs-symbol");
const elFsTf = document.getElementById("market-fs-timeframe");
const elFsLoad = document.getElementById("market-fs-load");
const elDivLegend = document.getElementById("market-div-legend");
const HUB_MARKET_POS_CTX_KEY = "hubMarketPosContext";
const EMA_FAST = 21;
@@ -69,7 +80,10 @@
macdSignal: null,
macdHist: null,
rsi: null,
rsi30: null,
rsi70: null,
};
let divergenceMarkers = [];
let chart = null;
let candleSeries = null;
@@ -306,7 +320,17 @@
function clearIndicatorSeries() {
if (!chart) return;
[indSeries.rsi30, indSeries.rsi70].forEach(function (pl) {
if (pl && indSeries.rsi) {
try {
indSeries.rsi.removePriceLine(pl);
} catch (e) {}
}
});
indSeries.rsi30 = null;
indSeries.rsi70 = null;
Object.keys(indSeries).forEach(function (k) {
if (k === "rsi30" || k === "rsi70") return;
if (indSeries[k]) {
try {
chart.removeSeries(indSeries[k]);
@@ -316,57 +340,216 @@
});
}
function applyScaleLayout() {
if (!chart) return;
function findSwings(values, lookback) {
const lows = [];
const highs = [];
const lb = lookback || SWING_LOOKBACK;
for (let i = lb; i < values.length - lb; i++) {
const v = values[i];
if (v == null || !Number.isFinite(v)) continue;
let isLow = true;
let isHigh = true;
for (let j = 1; j <= lb; j++) {
const lv = values[i - j];
const rv = values[i + j];
if (lv == null || rv == null || v > lv || v > rv) isLow = false;
if (lv == null || rv == null || v < lv || v < rv) isHigh = false;
}
if (isLow) lows.push({ i: i, v: v });
if (isHigh) highs.push({ i: i, v: v });
}
return { lows, highs };
}
function detectDivergences(candles, indicatorByIndex, sourceLabel) {
const markers = [];
if (!candles.length || !indicatorByIndex.length) return markers;
const closes = candles.map(function (c) {
return Number(c.close);
});
const priceSw = findSwings(closes, SWING_LOOKBACK);
const indSw = findSwings(indicatorByIndex, SWING_LOOKBACK);
function pushMarker(idx, kind, label) {
const c = candles[idx];
if (!c || c.time == null) return;
const bull = kind === "bull";
markers.push({
time: c.time,
position: bull ? "belowBar" : "aboveBar",
color: bull ? "#00ff9d" : "#ff4d6d",
shape: bull ? "arrowUp" : "arrowDown",
text: label,
});
}
const pLows = priceSw.lows;
const iLows = indSw.lows;
if (pLows.length >= 2 && iLows.length >= 2) {
const p1 = pLows[pLows.length - 2];
const p2 = pLows[pLows.length - 1];
const i1 = iLows[iLows.length - 2];
const i2 = iLows[iLows.length - 1];
if (Math.abs(p1.i - i1.i) < 30 && Math.abs(p2.i - i2.i) < 30) {
if (p2.v < p1.v && i2.v > i1.v) {
pushMarker(p2.i, "bull", sourceLabel + "底背离");
}
}
}
const pHighs = priceSw.highs;
const iHighs = indSw.highs;
if (pHighs.length >= 2 && iHighs.length >= 2) {
const p1 = pHighs[pHighs.length - 2];
const p2 = pHighs[pHighs.length - 1];
const i1 = iHighs[iHighs.length - 2];
const i2 = iHighs[iHighs.length - 1];
if (Math.abs(p1.i - i1.i) < 30 && Math.abs(p2.i - i2.i) < 30) {
if (p2.v > p1.v && i2.v < i1.v) {
pushMarker(p2.i, "bear", sourceLabel + "顶背离");
}
}
}
return markers.slice(-MAX_DIV_MARKERS);
}
function buildRsiByIndex(candles, period) {
const series = buildRsiSeries(candles, period);
const byIdx = new Array(candles.length).fill(null);
let si = 0;
for (let i = 0; i < candles.length; i++) {
if (si < series.length && series[si].time === candles[i].time) {
byIdx[i] = series[si].value;
si++;
}
}
return { series, byIdx };
}
function buildMacdByIndex(candles) {
const closes = candles.map(function (c) {
return Number(c.close);
});
const ema12 = emaArray(closes, 12);
const ema26 = emaArray(closes, 26);
const macd = new Array(closes.length).fill(null);
for (let i = 0; i < closes.length; i++) {
if (ema12[i] == null || ema26[i] == null) continue;
macd[i] = ema12[i] - ema26[i];
}
return macd;
}
function panelLayout() {
const rsiOn = indicatorState.rsi;
const macdOn = indicatorState.macd;
let candleBottom = CANDLE_SCALE_BOTTOM;
let volTop = VOLUME_SCALE_TOP;
const volBottom = VOLUME_SCALE_BOTTOM;
if (!rsiOn && !macdOn) {
return {
candleBottom: CANDLE_SCALE_BOTTOM,
volTop: VOLUME_SCALE_TOP,
volBottom: VOLUME_SCALE_BOTTOM,
macdTop: null,
macdBottom: null,
rsiTop: null,
rsiBottom: null,
rsiOn: false,
macdOn: false,
};
}
let stackH = 0;
if (rsiOn) stackH += PANEL_RSI_H;
if (macdOn) stackH += PANEL_MACD_H;
const volBottom = VOLUME_SCALE_BOTTOM + stackH;
const volTop = volBottom + PANEL_VOL_H;
let rsiTop = null;
let rsiBottom = null;
let macdTop = null;
let macdBottom = null;
let cursor = VOLUME_SCALE_BOTTOM;
if (rsiOn) {
rsiBottom = cursor;
rsiTop = cursor + PANEL_RSI_H;
cursor = rsiTop;
}
if (macdOn) {
macdBottom = cursor;
macdTop = cursor + PANEL_MACD_H;
cursor = macdTop;
}
const candleBottom = Math.max(CANDLE_SCALE_BOTTOM, volTop + 0.02);
return {
candleBottom,
volTop,
volBottom,
macdTop,
macdBottom,
rsiTop,
rsiBottom,
rsiOn,
macdOn,
};
}
if (rsiOn && macdOn) {
candleBottom = 0.52;
volTop = 0.84;
chart.priceScale("rsi").applyOptions({
scaleMargins: { top: 0.66, bottom: 0.18 },
borderColor: "#2a4058",
autoScale: true,
});
function applyScaleLayout() {
if (!chart) return;
const L = panelLayout();
chart.priceScale("right").applyOptions({
scaleMargins: { top: 0.06, bottom: L.candleBottom },
});
chart.priceScale("volume").applyOptions({
scaleMargins: { top: L.volTop, bottom: L.volBottom },
});
if (L.macdOn && L.macdTop != null) {
chart.priceScale("macd").applyOptions({
scaleMargins: { top: 0.48, bottom: 0.34 },
borderColor: "#2a4058",
autoScale: true,
});
} else if (rsiOn) {
candleBottom = 0.4;
volTop = 0.78;
chart.priceScale("rsi").applyOptions({
scaleMargins: { top: 0.62, bottom: 0.22 },
borderColor: "#2a4058",
autoScale: true,
});
} else if (macdOn) {
candleBottom = 0.42;
volTop = 0.78;
chart.priceScale("macd").applyOptions({
scaleMargins: { top: 0.44, bottom: 0.3 },
scaleMargins: { top: L.macdTop, bottom: L.macdBottom },
borderColor: "#2a4058",
autoScale: true,
});
}
if (L.rsiOn && L.rsiTop != null) {
chart.priceScale("rsi").applyOptions({
scaleMargins: { top: L.rsiTop, bottom: L.rsiBottom },
borderColor: "#2a4058",
autoScale: true,
});
}
}
chart.priceScale("right").applyOptions({
scaleMargins: { top: 0.06, bottom: candleBottom },
});
chart.priceScale("volume").applyOptions({
scaleMargins: { top: volTop, bottom: volBottom },
});
function updateDivergenceLegend(rsiDiv, macdDiv) {
if (!elDivLegend) return;
const parts = [];
if (indicatorState.rsi && rsiDiv.length) {
parts.push("RSI " + rsiDiv.map(function (m) { return m.text; }).join(" · "));
}
if (indicatorState.macd && macdDiv.length) {
parts.push("MACD " + macdDiv.map(function (m) { return m.text; }).join(" · "));
}
if (!parts.length) {
elDivLegend.textContent = "";
elDivLegend.classList.add("hidden");
return;
}
elDivLegend.textContent = parts.join(" ");
elDivLegend.classList.remove("hidden");
}
function applyCandleDivergenceMarkers() {
if (!candleSeries || !candleSeries.setMarkers) return;
const sorted = divergenceMarkers
.slice()
.sort(function (a, b) {
return a.time > b.time ? 1 : a.time < b.time ? -1 : 0;
});
candleSeries.setMarkers(sorted);
}
function updateIndicators() {
if (!chart || !lastCandles.length) return;
readIndicatorState();
clearIndicatorSeries();
divergenceMarkers = [];
applyScaleLayout();
if (indicatorState.ema) {
@@ -387,8 +570,12 @@
if (indSeries.ema55) indSeries.ema55.setData(buildEmaSeries(lastCandles, EMA_SLOW));
}
let rsiDiv = [];
let macdDiv = [];
if (indicatorState.macd) {
const macd = buildMacdData(lastCandles);
const macdByIdx = buildMacdByIndex(lastCandles);
indSeries.macdLine = createLineSeries({
color: "#5b9cf5",
title: "MACD",
@@ -403,30 +590,84 @@
if (indSeries.macdLine) indSeries.macdLine.setData(macd.macdLine);
if (indSeries.macdSignal) indSeries.macdSignal.setData(macd.signalLine);
if (indSeries.macdHist) indSeries.macdHist.setData(macd.histData);
macdDiv = detectDivergences(lastCandles, macdByIdx, "MACD");
divergenceMarkers = divergenceMarkers.concat(macdDiv);
}
if (indicatorState.rsi) {
const rsiPack = buildRsiByIndex(lastCandles, 14);
indSeries.rsi = createLineSeries({
color: "#8fc8ff",
title: "RSI",
priceScaleId: "rsi",
priceFormat: { type: "price", precision: 1, minMove: 0.1 },
});
if (indSeries.rsi) indSeries.rsi.setData(buildRsiSeries(lastCandles, 14));
if (indSeries.rsi) {
indSeries.rsi.setData(rsiPack.series);
try {
indSeries.rsi30 = indSeries.rsi.createPriceLine({
price: 30,
color: "rgba(255, 77, 109, 0.75)",
lineWidth: 1,
lineStyle: 2,
axisLabelVisible: true,
title: "30",
});
indSeries.rsi70 = indSeries.rsi.createPriceLine({
price: 70,
color: "rgba(0, 255, 157, 0.75)",
lineWidth: 1,
lineStyle: 2,
axisLabelVisible: true,
title: "70",
});
} catch (e) {}
}
rsiDiv = detectDivergences(lastCandles, rsiPack.byIdx, "RSI");
divergenceMarkers = divergenceMarkers.concat(rsiDiv);
}
updateDivergenceLegend(rsiDiv, macdDiv);
applyCandleDivergenceMarkers();
scheduleChartResize();
}
function syncFsToolbarFromMain() {
if (!chartFullscreen) return;
if (elFsExchange && elExchange) elFsExchange.value = elExchange.value;
if (elFsSymbol && elSymbol) elFsSymbol.value = elSymbol.value;
if (elFsTf && elTf) elFsTf.value = elTf.value;
}
function syncMainFromFsToolbar() {
if (elExchange && elFsExchange) elExchange.value = elFsExchange.value;
if (elSymbol && elFsSymbol) elSymbol.value = elFsSymbol.value.trim().toUpperCase();
if (elTf && elFsTf) elTf.value = elFsTf.value;
updateExchangeDisplay();
updateHeaderLabels(elSymbol && elSymbol.value, elTf && elTf.value);
}
function populateFsExchangeOptions() {
if (!elFsExchange || !elExchange) return;
elFsExchange.innerHTML = elExchange.innerHTML;
elFsExchange.value = elExchange.value;
}
function setChartFullscreen(on) {
chartFullscreen = !!on;
const wrap = elChartWrap || (chartHost && chartHost.closest(".market-chart-wrap"));
if (wrap) wrap.classList.toggle("is-fullscreen", chartFullscreen);
document.body.classList.toggle("market-chart-fs-open", chartFullscreen);
if (elFsToolbar) elFsToolbar.classList.toggle("hidden", !chartFullscreen);
if (elFsBtn) elFsBtn.textContent = chartFullscreen ? "退出全屏" : "全屏";
if (elFsExit) {
if (chartFullscreen) elFsExit.classList.remove("hidden");
else elFsExit.classList.add("hidden");
}
if (chartFullscreen) {
populateFsExchangeOptions();
syncFsToolbarFromMain();
}
scheduleChartResize();
}
@@ -1013,6 +1254,7 @@
opt.textContent = ex.name || ex.key;
elExchange.appendChild(opt);
});
populateFsExchangeOptions();
readQuery();
applyDefaults();
updateExchangeDisplay();
@@ -1123,12 +1365,14 @@
elTf.addEventListener("change", function () {
currentTf = (elTf && elTf.value) || "1d";
tickLiveClock();
syncFsToolbarFromMain();
loadChart(false);
});
}
if (elExchange) {
elExchange.addEventListener("change", function () {
updateExchangeDisplay();
syncFsToolbarFromMain();
loadChart(false);
});
}
@@ -1173,6 +1417,34 @@
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && chartFullscreen) setChartFullscreen(false);
});
if (elFsExchange) {
elFsExchange.addEventListener("change", function () {
syncMainFromFsToolbar();
loadChart(false);
});
}
if (elFsTf) {
elFsTf.addEventListener("change", function () {
currentTf = elFsTf.value || "1d";
syncMainFromFsToolbar();
tickLiveClock();
loadChart(false);
});
}
if (elFsSymbol) {
elFsSymbol.addEventListener("keydown", function (e) {
if (e.key === "Enter") {
syncMainFromFsToolbar();
loadChart(false);
}
});
}
if (elFsLoad) {
elFsLoad.addEventListener("click", function () {
syncMainFromFsToolbar();
loadChart(false);
});
}
}
window.hubMarketChart = {