三周期分开关控制,只画数字不画图标;线段面积比沿用同向笔面积口径。 Co-authored-by: Cursor <cursoragent@cursor.com>
2543 lines
143 KiB
JavaScript
2543 lines
143 KiB
JavaScript
/* chart_tv_overlays.js — structure zones / BSP / FX / bollinger */
|
||
|
||
/** 标记 time 必须落在主 series 的 K 线 time 上,否则 LWC 会抛 Value is null */
|
||
function alignMarkersToCandles(markers, candles) {
|
||
if (!Array.isArray(markers) || !markers.length) return [];
|
||
if (!Array.isArray(candles) || !candles.length) return [];
|
||
var times = [];
|
||
for (var i = 0; i < candles.length; i++) {
|
||
var ct = candles[i] && candles[i].time;
|
||
if (ct == null || !isFinite(Number(ct))) continue;
|
||
times.push(Math.floor(Number(ct)));
|
||
}
|
||
if (!times.length) return [];
|
||
var set = {};
|
||
for (var j = 0; j < times.length; j++) set[times[j]] = true;
|
||
var nearest = function (target) {
|
||
var best = times[0];
|
||
var bestDiff = Math.abs(best - target);
|
||
// 两端夹逼:大数据量时比全扫略好
|
||
var lo = 0, hi = times.length - 1;
|
||
while (lo <= hi) {
|
||
var mid = (lo + hi) >> 1;
|
||
var t = times[mid];
|
||
var d = Math.abs(t - target);
|
||
if (d < bestDiff) { best = t; bestDiff = d; }
|
||
if (t < target) lo = mid + 1;
|
||
else hi = mid - 1;
|
||
}
|
||
if (lo < times.length) {
|
||
var d2 = Math.abs(times[lo] - target);
|
||
if (d2 < bestDiff) best = times[lo];
|
||
}
|
||
if (hi >= 0) {
|
||
var d3 = Math.abs(times[hi] - target);
|
||
if (d3 < bestDiff) best = times[hi];
|
||
}
|
||
return best;
|
||
};
|
||
var out = [];
|
||
for (var k = 0; k < markers.length; k++) {
|
||
var m = markers[k];
|
||
if (!m || m.time == null || !isFinite(Number(m.time))) continue;
|
||
var t0 = Math.floor(Number(m.time));
|
||
var aligned = set[t0] ? t0 : nearest(t0);
|
||
var copy = Object.assign({}, m, { time: aligned });
|
||
out.push(copy);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/** 解析后端 KLC trend(UP/DOWN/FLAT 或枚举名/数字) */
|
||
function normalizeKlcTrendRaw(trend) {
|
||
var raw = (trend == null ? '' : String(trend)).trim();
|
||
if (!raw) return 'UNKNOWN';
|
||
var upper = raw.toUpperCase();
|
||
if (upper === 'UP' || raw === '1' || upper.indexOf('.UP') >= 0 || upper.endsWith('UP')) return 'UP';
|
||
if (upper === 'DOWN' || raw === '2' || upper.indexOf('.DOWN') >= 0 || upper.endsWith('DOWN')) return 'DOWN';
|
||
if (upper === 'FLAT' || raw === '3' || upper.indexOf('.FLAT') >= 0 || upper.endsWith('FLAT')) return 'FLAT';
|
||
return upper;
|
||
}
|
||
|
||
var MAIN_KLC_TREND_STYLE = {
|
||
UP: { position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 },
|
||
DOWN: { position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 },
|
||
FLAT: { position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 },
|
||
UNKNOWN: { position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 }
|
||
};
|
||
|
||
/** 次周期 Trend:橙涨 / 靛蓝跌,与主周期绿红区分 */
|
||
var ELEMENT_KLC_TREND_STYLE = {
|
||
UP: { position: 'aboveBar', color: '#e67e22', shape: 'arrowUp', size: 0.5 },
|
||
DOWN: { position: 'belowBar', color: '#5c6bc0', shape: 'arrowDown', size: 0.5 },
|
||
FLAT: { position: 'inBar', color: '#bdbdbd', shape: 'circle', size: 0.8 },
|
||
UNKNOWN: { position: 'inBar', color: '#8e44ad', shape: 'square', size: 0.8 }
|
||
};
|
||
|
||
/** 次次周期 Trend:青绿涨 / 灰蓝跌(略小标记) */
|
||
var SUB_SUB_KLC_TREND_STYLE = {
|
||
UP: { position: 'aboveBar', color: '#00b894', shape: 'arrowUp', size: 0.4 },
|
||
DOWN: { position: 'belowBar', color: '#546e7a', shape: 'arrowDown', size: 0.4 },
|
||
FLAT: { position: 'inBar', color: '#00897b', shape: 'circle', size: 0.5 },
|
||
UNKNOWN: { position: 'inBar', color: '#004d40', shape: 'square', size: 0.5 }
|
||
};
|
||
|
||
function parseAreaDivValue(v) {
|
||
var n = Number(v);
|
||
return (isFinite(n) && n !== 0) ? n : 0;
|
||
}
|
||
|
||
function pushAreaTextLabel(out, item, style, text) {
|
||
if (!item || !item.end_time || !text) return;
|
||
var ts = Math.floor(new Date(item.end_time).getTime() / 1000);
|
||
if (isNaN(ts)) return;
|
||
var price = Number(item.end_price);
|
||
if (!isFinite(price)) price = Number(item.start_price);
|
||
if (!isFinite(price)) return;
|
||
var up = Number(item.direction) === 1;
|
||
out.push({
|
||
time: ts,
|
||
price: price,
|
||
text: text,
|
||
color: style.color,
|
||
above: up
|
||
});
|
||
}
|
||
|
||
function pushAreaDivMarker(out, item, style) {
|
||
var div = parseAreaDivValue(item && item.macd_div);
|
||
if (!div) return;
|
||
pushAreaTextLabel(out, item, style, div.toFixed(2));
|
||
}
|
||
|
||
function collectAreaDivMarkers(biList, uncompletedBi, segList, uncompletedSeg, biStyle, segStyle, showBi, showSeg) {
|
||
var out = [];
|
||
if (showBi) {
|
||
(biList || []).forEach(function (bi) { pushAreaDivMarker(out, bi, biStyle); });
|
||
(uncompletedBi || []).forEach(function (bi) { pushAreaDivMarker(out, bi, biStyle); });
|
||
}
|
||
if (showSeg) {
|
||
(segList || []).forEach(function (seg) { pushAreaDivMarker(out, seg, segStyle); });
|
||
(uncompletedSeg || []).forEach(function (seg) { pushAreaDivMarker(out, seg, segStyle); });
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function formatMacdAreaText(v) {
|
||
var n = Number(v);
|
||
if (!isFinite(n) || n === 0) return '';
|
||
var abs = Math.abs(n);
|
||
if (abs >= 100) return n.toFixed(0);
|
||
if (abs >= 10) return n.toFixed(1);
|
||
return n.toFixed(2);
|
||
}
|
||
|
||
function pushAreaHistMarker(out, item, style) {
|
||
pushAreaTextLabel(out, item, style, formatMacdAreaText(item && item.macd_hist));
|
||
}
|
||
|
||
function collectAreaHistMarkers(biList, uncompletedBi, segList, uncompletedSeg, biStyle, segStyle, showBi, showSeg) {
|
||
var out = [];
|
||
if (showBi) {
|
||
(biList || []).forEach(function (bi) { pushAreaHistMarker(out, bi, biStyle); });
|
||
(uncompletedBi || []).forEach(function (bi) { pushAreaHistMarker(out, bi, biStyle); });
|
||
}
|
||
if (showSeg) {
|
||
(segList || []).forEach(function (seg) { pushAreaHistMarker(out, seg, segStyle); });
|
||
(uncompletedSeg || []).forEach(function (seg) { pushAreaHistMarker(out, seg, segStyle); });
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function buildAreaHistMarkersFromData(data) {
|
||
var markers = [];
|
||
if (!data) return markers;
|
||
var showMainBi = $('#showMainBiArea').is(':checked');
|
||
var showMainSeg = $('#showMainSegArea').is(':checked');
|
||
if (showMainBi || showMainSeg) {
|
||
markers = markers.concat(collectAreaHistMarkers(
|
||
data.bi_list,
|
||
data.uncompleted_bi_list,
|
||
data.seg_list,
|
||
data.uncompleted_seg_list,
|
||
{ color: '#1565c0', size: 0.55 },
|
||
{ color: '#00838f', size: 0.65 },
|
||
showMainBi,
|
||
showMainSeg
|
||
));
|
||
}
|
||
var showElementBi = $('#showElementBiArea').is(':checked');
|
||
var showElementSeg = $('#showElementSegArea').is(':checked');
|
||
if (showElementBi || showElementSeg) {
|
||
markers = markers.concat(collectAreaHistMarkers(
|
||
data.element_bi_list,
|
||
data.element_uncompleted_bi_list,
|
||
data.element_seg_list,
|
||
data.element_uncompleted_seg_list,
|
||
{ color: '#3949ab', size: 0.5 },
|
||
{ color: '#5c6bc0', size: 0.6 },
|
||
showElementBi,
|
||
showElementSeg
|
||
));
|
||
}
|
||
var showSubSubBi = $('#showSubSubBiArea').is(':checked');
|
||
var showSubSubSeg = $('#showSubSubSegArea').is(':checked');
|
||
if (showSubSubBi || showSubSubSeg) {
|
||
markers = markers.concat(collectAreaHistMarkers(
|
||
data.sub_sub_bi_list,
|
||
data.sub_sub_uncompleted_bi_list,
|
||
data.sub_sub_seg_list,
|
||
data.sub_sub_uncompleted_seg_list,
|
||
{ color: '#2e7d32', size: 0.45 },
|
||
{ color: '#558b2f', size: 0.55 },
|
||
showSubSubBi,
|
||
showSubSubSeg
|
||
));
|
||
}
|
||
return markers;
|
||
}
|
||
|
||
function buildAreaDivMarkersFromData(data) {
|
||
var markers = [];
|
||
if (!data) return markers;
|
||
var showMainBi = $('#showMainMacdDiv').is(':checked');
|
||
var showMainSeg = $('#showMainSegMacdDiv').is(':checked');
|
||
if (showMainBi || showMainSeg) {
|
||
markers = markers.concat(collectAreaDivMarkers(
|
||
data.bi_list,
|
||
data.uncompleted_bi_list,
|
||
data.seg_list,
|
||
data.uncompleted_seg_list,
|
||
{ color: '#e53935', size: 0.6 },
|
||
{ color: '#fb8c00', size: 0.7 },
|
||
showMainBi,
|
||
showMainSeg
|
||
));
|
||
}
|
||
var showElementBi = $('#showElementMacdDiv').is(':checked');
|
||
var showElementSeg = $('#showElementSegMacdDiv').is(':checked');
|
||
if (showElementBi || showElementSeg) {
|
||
markers = markers.concat(collectAreaDivMarkers(
|
||
data.element_bi_list,
|
||
data.element_uncompleted_bi_list,
|
||
data.element_seg_list,
|
||
data.element_uncompleted_seg_list,
|
||
{ color: '#8e24aa', size: 0.55 },
|
||
{ color: '#5e35b1', size: 0.65 },
|
||
showElementBi,
|
||
showElementSeg
|
||
));
|
||
}
|
||
var showSubSubBi = $('#showSubSubMacdDiv').is(':checked');
|
||
var showSubSubSeg = $('#showSubSubSegMacdDiv').is(':checked');
|
||
if (showSubSubBi || showSubSubSeg) {
|
||
markers = markers.concat(collectAreaDivMarkers(
|
||
data.sub_sub_bi_list,
|
||
data.sub_sub_uncompleted_bi_list,
|
||
data.sub_sub_seg_list,
|
||
data.sub_sub_uncompleted_seg_list,
|
||
{ color: '#00897b', size: 0.5 },
|
||
{ color: '#00695c', size: 0.6 },
|
||
showSubSubBi,
|
||
showSubSubSeg
|
||
));
|
||
}
|
||
return markers;
|
||
}
|
||
|
||
function buildKlcTrendMarker(timeAligned, trendRaw, palette) {
|
||
var kind = normalizeKlcTrendRaw(trendRaw);
|
||
var style = palette[kind] || palette.UNKNOWN || palette.FLAT;
|
||
return {
|
||
time: timeAligned,
|
||
position: style.position,
|
||
color: style.color,
|
||
shape: style.shape,
|
||
size: style.size
|
||
};
|
||
}
|
||
|
||
function safeOverlayLineSetData(series, points) {
|
||
if (!series || typeof series.setData !== 'function' || !Array.isArray(points) || points.length < 2) return;
|
||
try {
|
||
var a = points[0], b = points[1];
|
||
if (!a || !b || a.time == null || b.time == null) return;
|
||
var t0 = Math.floor(Number(a.time));
|
||
var t1 = Math.floor(Number(b.time));
|
||
var v0 = Number(a.value);
|
||
var v1 = Number(b.value);
|
||
if (!isFinite(t0) || !isFinite(t1) || !isFinite(v0) || !isFinite(v1)) return;
|
||
// 竖边不用折线(任意时间差都会斜),改走 canvas
|
||
if (t0 === t1) return;
|
||
if (t0 > t1) {
|
||
series.setData([{ time: t1, value: v1 }, { time: t0, value: v0 }]);
|
||
} else {
|
||
series.setData([{ time: t0, value: v0 }, { time: t1, value: v1 }]);
|
||
}
|
||
} catch (e) {
|
||
console.warn('叠层线 setData 跳过:', e && e.message ? e.message : e);
|
||
}
|
||
}
|
||
|
||
function pushFxBoxVertical(time, lo, hi, color) {
|
||
if (!window._fxBoxVerticals) window._fxBoxVerticals = [];
|
||
var t = Math.floor(Number(time));
|
||
var a = Number(lo), b = Number(hi);
|
||
if (!isFinite(t) || !isFinite(a) || !isFinite(b) || a === b) return;
|
||
window._fxBoxVerticals.push({
|
||
time: t,
|
||
lo: Math.min(a, b),
|
||
hi: Math.max(a, b),
|
||
color: color || '#888'
|
||
});
|
||
}
|
||
|
||
function getMainPriceSeries() {
|
||
if (!window.tvWidget || !tvWidget.series) return null;
|
||
var s = tvWidget.series;
|
||
return s.candleSeries || s.klcSeries || s.barSeries || s.heikinSeries || s.renkoSeries ||
|
||
s.lineSeries || s.areaSeries || s.baselineSeries || null;
|
||
}
|
||
|
||
function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) {
|
||
if (!mainChart || !mainChartContainer) return;
|
||
if (typeof window._fxBoxOverlayCleanup === 'function') {
|
||
try { window._fxBoxOverlayCleanup(); } catch (e) {}
|
||
window._fxBoxOverlayCleanup = null;
|
||
}
|
||
var canvas = mainChartContainer.querySelector('.fx-box-vert-overlay');
|
||
if (!canvas) {
|
||
canvas = document.createElement('canvas');
|
||
canvas.className = 'fx-box-vert-overlay';
|
||
canvas.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;z-index:6;';
|
||
if (getComputedStyle(mainChartContainer).position === 'static') {
|
||
mainChartContainer.style.position = 'relative';
|
||
}
|
||
mainChartContainer.appendChild(canvas);
|
||
}
|
||
var lastSig = '';
|
||
var watchRaf = null;
|
||
var cleaned = false;
|
||
var redrawPending = false;
|
||
var quant = function (v) {
|
||
if (v == null || !isFinite(Number(v))) return 'n';
|
||
return String(Math.round(Number(v)));
|
||
};
|
||
// LWC 4 无 priceScale 订阅:采样坐标变化(含增量 setData 后自动缩放)
|
||
var sampleSig = function () {
|
||
var boxes = window._fxBoxVerticals || [];
|
||
var labels = window._areaTextLabels || [];
|
||
var series = getMainPriceSeries();
|
||
if (!series || (!boxes.length && !labels.length)) return '0';
|
||
var ts = mainChart.timeScale();
|
||
var parts = [boxes.length, labels.length];
|
||
if (boxes.length) {
|
||
var a = boxes[0];
|
||
var b = boxes[boxes.length - 1];
|
||
parts.push(
|
||
quant(ts.timeToCoordinate(a.time)),
|
||
quant(series.priceToCoordinate(a.hi)),
|
||
quant(series.priceToCoordinate(a.lo)),
|
||
quant(ts.timeToCoordinate(b.time)),
|
||
quant(series.priceToCoordinate(b.hi)),
|
||
quant(series.priceToCoordinate(b.lo))
|
||
);
|
||
}
|
||
if (labels.length) {
|
||
var la = labels[0];
|
||
var lb = labels[labels.length - 1];
|
||
parts.push(
|
||
quant(ts.timeToCoordinate(la.time)),
|
||
quant(series.priceToCoordinate(la.price)),
|
||
quant(ts.timeToCoordinate(lb.time)),
|
||
quant(series.priceToCoordinate(lb.price))
|
||
);
|
||
}
|
||
return parts.join('|');
|
||
};
|
||
var redraw = function () {
|
||
var boxes = window._fxBoxVerticals || [];
|
||
var labels = window._areaTextLabels || [];
|
||
var series = getMainPriceSeries();
|
||
var rect = mainChartContainer.getBoundingClientRect();
|
||
var dpr = window.devicePixelRatio || 1;
|
||
canvas.width = Math.max(1, Math.floor(rect.width * dpr));
|
||
canvas.height = Math.max(1, Math.floor(rect.height * dpr));
|
||
canvas.style.width = rect.width + 'px';
|
||
canvas.style.height = rect.height + 'px';
|
||
var ctx2 = canvas.getContext('2d');
|
||
if (!ctx2) return;
|
||
ctx2.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||
ctx2.clearRect(0, 0, rect.width, rect.height);
|
||
if (!series || (!boxes.length && !labels.length)) {
|
||
lastSig = sampleSig();
|
||
return;
|
||
}
|
||
var ts = mainChart.timeScale();
|
||
for (var i = 0; i < boxes.length; i++) {
|
||
var box = boxes[i];
|
||
var x = ts.timeToCoordinate(box.time);
|
||
var y1 = series.priceToCoordinate(box.hi);
|
||
var y2 = series.priceToCoordinate(box.lo);
|
||
if (x == null || y1 == null || y2 == null) continue;
|
||
ctx2.beginPath();
|
||
ctx2.strokeStyle = box.color;
|
||
ctx2.lineWidth = 1;
|
||
ctx2.setLineDash([4, 3]);
|
||
ctx2.moveTo(Math.round(x) + 0.5, y1);
|
||
ctx2.lineTo(Math.round(x) + 0.5, y2);
|
||
ctx2.stroke();
|
||
}
|
||
ctx2.setLineDash([]);
|
||
if (labels.length) {
|
||
ctx2.font = '11px sans-serif';
|
||
ctx2.textAlign = 'center';
|
||
for (var li = 0; li < labels.length; li++) {
|
||
var lab = labels[li];
|
||
var lx = ts.timeToCoordinate(lab.time);
|
||
var ly = series.priceToCoordinate(lab.price);
|
||
if (lx == null || ly == null) continue;
|
||
ctx2.fillStyle = lab.color;
|
||
ctx2.textBaseline = lab.above ? 'bottom' : 'top';
|
||
ctx2.fillText(lab.text, Math.round(lx), lab.above ? ly - 3 : ly + 3);
|
||
}
|
||
}
|
||
lastSig = sampleSig();
|
||
};
|
||
var scheduleRedraw = function () {
|
||
if (cleaned || redrawPending) return;
|
||
redrawPending = true;
|
||
requestAnimationFrame(function () {
|
||
redrawPending = false;
|
||
if (!cleaned) redraw();
|
||
});
|
||
};
|
||
var watch = function () {
|
||
if (cleaned) return;
|
||
watchRaf = requestAnimationFrame(watch);
|
||
var sig = sampleSig();
|
||
if (sig !== lastSig) scheduleRedraw();
|
||
};
|
||
try { mainChart.timeScale().subscribeVisibleLogicalRangeChange(scheduleRedraw); } catch (e) {}
|
||
try { mainChart.timeScale().subscribeVisibleTimeRangeChange(scheduleRedraw); } catch (e) {}
|
||
var ro = null;
|
||
if (typeof ResizeObserver !== 'undefined') {
|
||
ro = new ResizeObserver(scheduleRedraw);
|
||
ro.observe(mainChartContainer);
|
||
}
|
||
window._redrawFxBoxVerticalOverlay = scheduleRedraw;
|
||
window._fxBoxOverlayCleanup = function () {
|
||
if (cleaned) return;
|
||
cleaned = true;
|
||
if (watchRaf != null) {
|
||
try { cancelAnimationFrame(watchRaf); } catch (e) {}
|
||
watchRaf = null;
|
||
}
|
||
window._redrawFxBoxVerticalOverlay = null;
|
||
try { mainChart.timeScale().unsubscribeVisibleLogicalRangeChange(scheduleRedraw); } catch (e) {}
|
||
try { mainChart.timeScale().unsubscribeVisibleTimeRangeChange(scheduleRedraw); } catch (e) {}
|
||
if (ro) try { ro.disconnect(); } catch (e) {}
|
||
try { if (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas); } catch (e) {}
|
||
};
|
||
if (!window._tvInitCleanups) window._tvInitCleanups = [];
|
||
window._tvInitCleanups.push(window._fxBoxOverlayCleanup);
|
||
scheduleRedraw();
|
||
setTimeout(scheduleRedraw, 50);
|
||
watchRaf = requestAnimationFrame(watch);
|
||
}
|
||
|
||
function chartTvRenderOverlays(ctx) {
|
||
window._fxBoxVerticals = [];
|
||
window._areaTextLabels = [];
|
||
var symbol = ctx.symbol;
|
||
var timeframe = ctx.timeframe;
|
||
var symbolConfig = ctx.symbolConfig;
|
||
var useSubSubPeriod = ctx.useSubSubPeriod;
|
||
var useElementPeriod = ctx.useElementPeriod;
|
||
var klinePeriodLabel = ctx.klinePeriodLabel;
|
||
var candles = ctx.candles;
|
||
var klineDataSource = ctx.klineDataSource;
|
||
var container = ctx.container;
|
||
var showMacd = ctx.showMacd;
|
||
var showOriginalKline = ctx.showOriginalKline;
|
||
var mainChartContainer = ctx.mainChartContainer;
|
||
var volumeChartContainer = ctx.volumeChartContainer;
|
||
var atrChartContainer = ctx.atrChartContainer;
|
||
var macdChartContainer = ctx.macdChartContainer;
|
||
var chanMacdChartContainer = ctx.chanMacdChartContainer;
|
||
var mainChart = ctx.mainChart;
|
||
var volumeChart = ctx.volumeChart;
|
||
var atrChart = ctx.atrChart;
|
||
var macdChart = ctx.macdChart;
|
||
var chanMacdChart = ctx.chanMacdChart;
|
||
var createChartOptions = ctx.createChartOptions;
|
||
// 结构价值区绘制(半透明填充区 + 边框)
|
||
if ($('#showMainStructureZone').is(':checked') && currentData.structure_zones && currentData.structure_zones.length > 0) {
|
||
try {
|
||
const kd = currentData.kline_data || [];
|
||
if (kd.length > 0) {
|
||
const chartStart = Math.floor(new Date(kd[0].date).getTime() / 1000);
|
||
const chartEnd = Math.floor(new Date(kd[kd.length-1].date).getTime() / 1000);
|
||
// 计算可见价格范围,过滤超出范围的区间
|
||
let priceMin = Infinity, priceMax = -Infinity;
|
||
kd.forEach(function(k) {
|
||
const hi = parseFloat(k.high), lo = parseFloat(k.low);
|
||
if (!isNaN(hi) && hi > priceMax) priceMax = hi;
|
||
if (!isNaN(lo) && lo < priceMin) priceMin = lo;
|
||
});
|
||
const priceMargin = (priceMax - priceMin) * 0.05;
|
||
priceMin -= priceMargin;
|
||
priceMax += priceMargin;
|
||
let drawnCount = 0;
|
||
currentData.structure_zones.forEach(function(zone) {
|
||
try {
|
||
// 跳过完全超出可视价格范围的区间
|
||
if (zone.upper < priceMin || zone.lower > priceMax) return;
|
||
const fillColor = zone.zone_type === 'support' ? 'rgba(46, 204, 113, 0.08)' :
|
||
zone.zone_type === 'resistance' ? 'rgba(231, 76, 60, 0.08)' :
|
||
'rgba(149, 165, 166, 0.06)';
|
||
const borderColor = zone.zone_type === 'support' ? 'rgba(46, 204, 113, 0.7)' :
|
||
zone.zone_type === 'resistance' ? 'rgba(231, 76, 60, 0.7)' :
|
||
'rgba(149, 165, 166, 0.6)';
|
||
// 填充区:在上下边界之间画多条半透明线模拟填充
|
||
const fillLines = 8;
|
||
const step = (zone.upper - zone.lower) / (fillLines + 1);
|
||
for (let fi = 1; fi <= fillLines; fi++) {
|
||
const fy = zone.lower + step * fi;
|
||
mainChart.addLineSeries({ color: fillColor, lineWidth: 2, lineStyle: 0, lastValueVisible: false, priceLineVisible: false })
|
||
.setData([{ time: chartStart, value: fy }, { time: chartEnd, value: fy }]);
|
||
}
|
||
// 上边界(粗线)
|
||
mainChart.addLineSeries({ color: borderColor, lineWidth: 2, lineStyle: 0, lastValueVisible: false, priceLineVisible: false })
|
||
.setData([{ time: chartStart, value: zone.upper }, { time: chartEnd, value: zone.upper }]);
|
||
// 下边界(粗线)
|
||
mainChart.addLineSeries({ color: borderColor, lineWidth: 2, lineStyle: 0, lastValueVisible: false, priceLineVisible: false })
|
||
.setData([{ time: chartStart, value: zone.lower }, { time: chartEnd, value: zone.lower }]);
|
||
// 中心线(虚线)
|
||
mainChart.addLineSeries({ color: borderColor, lineWidth: 1, lineStyle: 2, lastValueVisible: false, priceLineVisible: false })
|
||
.setData([{ time: chartStart, value: zone.center }, { time: chartEnd, value: zone.center }]);
|
||
drawnCount++;
|
||
} catch (e) { console.error('结构区绘制出错:', e); }
|
||
});
|
||
console.log(`结构区: 共${currentData.structure_zones.length}个, 绘制${drawnCount}个 (可见价格范围: ${priceMin.toFixed(0)}-${priceMax.toFixed(0)})`);
|
||
}
|
||
} catch (e) { console.error('结构区整体绘制出错:', e); }
|
||
}
|
||
// 显示未完成中枢 - 分别处理主周期、次周期和次次周期
|
||
if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked') || $('#showSubSubZs').is(':checked') || $('#showSubSubBiZs').is(':checked')) {
|
||
console.log('绘制未完成中枢 - 已启用');
|
||
// 显示BI中枢绘制(沿用中枢样式)
|
||
if ($('#showMainBiZs').is(':checked') || $('#showElementBiZs').is(':checked') || $('#showSubSubBiZs').is(':checked')) {
|
||
console.log('绘制BI中枢 - 已启用');
|
||
// 主周期 BI 中枢
|
||
console.log('主BI开关:', $('#showMainBiZs').is(':checked'), '数据长度:', currentData.bi_zs_list ? currentData.bi_zs_list.length : 0);
|
||
if ($('#showMainBiZs').is(':checked') && currentData.bi_zs_list && currentData.bi_zs_list.length > 0) {
|
||
console.log(`绘制主周期BI中枢数据,共${currentData.bi_zs_list.length}条`);
|
||
currentData.bi_zs_list.forEach(function(zs) {
|
||
try {
|
||
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
|
||
const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
|
||
if (isNaN(startTime) || isNaN(endTime)) { return; }
|
||
const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
|
||
if (isNaN(zg) || isNaN(zd)) { return; }
|
||
const color = '#9C27B0'; // 主周期BI中枢颜色(紫色)
|
||
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
|
||
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
|
||
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
|
||
const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]);
|
||
if (!isNaN(gg) && gg > 0) {
|
||
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
|
||
}
|
||
if (!isNaN(dd) && dd > 0) {
|
||
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
|
||
}
|
||
} catch (e) { console.error('主周期BI中枢处理出错:', e); }
|
||
});
|
||
}
|
||
// 主周期 未完成 BI 中枢
|
||
console.log('主未完成BI长度:', currentData.uncompleted_bi_zs_list ? currentData.uncompleted_bi_zs_list.length : 0);
|
||
if ($('#showMainBiZs').is(':checked') && currentData.uncompleted_bi_zs_list && currentData.uncompleted_bi_zs_list.length > 0) {
|
||
console.log(`绘制主周期未完成BI中枢数据,共${currentData.uncompleted_bi_zs_list.length}条`);
|
||
currentData.uncompleted_bi_zs_list.forEach(function(zs) {
|
||
try {
|
||
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
|
||
const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
|
||
if (isNaN(startTime) || isNaN(endTime)) { return; }
|
||
const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
|
||
if (isNaN(zg) || isNaN(zd)) { return; }
|
||
const color = '#9C27B0';
|
||
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
|
||
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
|
||
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
|
||
if (!isNaN(gg) && gg > 0) {
|
||
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
|
||
}
|
||
if (!isNaN(dd) && dd > 0) {
|
||
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
|
||
}
|
||
} catch (e) { console.error('主周期未完成BI中枢处理出错:', e); }
|
||
});
|
||
}
|
||
// 次周期 BI 中枢
|
||
console.log('次BI开关:', $('#showElementBiZs').is(':checked'), '数据长度:', currentData.element_bi_zs_list ? currentData.element_bi_zs_list.length : 0);
|
||
if ($('#showElementBiZs').is(':checked') && currentData.element_bi_zs_list && currentData.element_bi_zs_list.length > 0) {
|
||
console.log(`绘制次周期BI中枢数据,共${currentData.element_bi_zs_list.length}条`);
|
||
currentData.element_bi_zs_list.forEach(function(zs) {
|
||
try {
|
||
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
|
||
const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
|
||
if (isNaN(startTime) || isNaN(endTime)) { return; }
|
||
const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
|
||
if (isNaN(zg) || isNaN(zd)) { return; }
|
||
const color = '#8BC34A'; // 次周期BI中枢颜色(绿)
|
||
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
|
||
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
|
||
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
|
||
const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]);
|
||
if (!isNaN(gg) && gg > 0) {
|
||
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
|
||
}
|
||
if (!isNaN(dd) && dd > 0) {
|
||
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
|
||
}
|
||
} catch (e) { console.error('次周期BI中枢处理出错:', e); }
|
||
});
|
||
}
|
||
// 次周期 未完成 BI 中枢
|
||
console.log('次未完成BI长度:', currentData.element_uncompleted_bi_zs_list ? currentData.element_uncompleted_bi_zs_list.length : 0);
|
||
if ($('#showElementBiZs').is(':checked') && currentData.element_uncompleted_bi_zs_list && currentData.element_uncompleted_bi_zs_list.length > 0) {
|
||
console.log(`绘制次周期未完成BI中枢数据,共${currentData.element_uncompleted_bi_zs_list.length}条`);
|
||
currentData.element_uncompleted_bi_zs_list.forEach(function(zs) {
|
||
try {
|
||
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
|
||
const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
|
||
if (isNaN(startTime) || isNaN(endTime)) { return; }
|
||
const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
|
||
if (isNaN(zg) || isNaN(zd)) { return; }
|
||
const color = '#8BC34A';
|
||
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
|
||
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
|
||
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
|
||
if (!isNaN(gg) && gg > 0) {
|
||
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
|
||
}
|
||
if (!isNaN(dd) && dd > 0) {
|
||
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
|
||
}
|
||
} catch (e) { console.error('次周期未完成BI中枢处理出错:', e); }
|
||
});
|
||
}
|
||
// 次次周期 未完成 BI 中枢
|
||
if ($('#showSubSubBiZs').is(':checked') && currentData.sub_sub_uncompleted_bi_zs_list && currentData.sub_sub_uncompleted_bi_zs_list.length > 0) {
|
||
const kdBi = currentData.kline_data || [];
|
||
const endTimeBi = kdBi.length ? Math.floor(new Date(kdBi[kdBi.length-1].date).getTime() / 1000) : 0;
|
||
currentData.sub_sub_uncompleted_bi_zs_list.forEach(function(zs) {
|
||
try {
|
||
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
|
||
if (isNaN(startTime) || !endTimeBi) return;
|
||
const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd);
|
||
if (isNaN(zg) || isNaN(zd)) return;
|
||
const color = '#00897b';
|
||
mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zg }, { time: endTimeBi, value: zg }]);
|
||
mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: endTimeBi, value: zd }]);
|
||
mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
|
||
if (!isNaN(gg) && gg > 0) mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: gg }, { time: endTimeBi, value: gg }]);
|
||
if (!isNaN(dd) && dd > 0) mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: dd }, { time: endTimeBi, value: dd }]);
|
||
} catch (e) { console.error('次次周期未完成BI中枢处理出错:', e); }
|
||
});
|
||
}
|
||
}
|
||
// 主周期未完成中枢
|
||
if ($('#showMainZs').is(':checked') && currentData.uncompleted_zs_list && currentData.uncompleted_zs_list.length > 0) {
|
||
console.log(`绘制主周期未完成中枢数据,共${currentData.uncompleted_zs_list.length}条`);
|
||
|
||
currentData.uncompleted_zs_list.forEach(function(zs) {
|
||
try {
|
||
// 直接使用UTC时间戳(秒)
|
||
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
|
||
// 未完成中枢的结束时间设为当前K线的最后时间
|
||
const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
|
||
|
||
if (isNaN(startTime) || isNaN(endTime)) {
|
||
console.error('主周期未完成中枢时间转换错误:', zs.start_time);
|
||
return;
|
||
}
|
||
|
||
const zg = parseFloat(zs.zg); // 中枢上沿
|
||
const zd = parseFloat(zs.zd); // 中枢下沿
|
||
const gg = parseFloat(zs.gg); // 中枢高高
|
||
const dd = parseFloat(zs.dd); // 中枢低低
|
||
|
||
if (isNaN(zg) || isNaN(zd)) {
|
||
console.error('主周期未完成中枢价格转换错误:', zs.zg, zs.zd);
|
||
return;
|
||
}
|
||
|
||
// 创建未完成中枢上边界
|
||
const topSeries = mainChart.addLineSeries({
|
||
color: '#F1C40F', // 主周期中枢颜色
|
||
lineWidth: 1,
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
});
|
||
|
||
topSeries.setData([
|
||
{ time: startTime, value: zg },
|
||
{ time: endTime, value: zg }
|
||
]);
|
||
|
||
// 为下边界创建另一条线
|
||
const bottomSeries = mainChart.addLineSeries({
|
||
color: '#F1C40F', // 主周期中枢颜色
|
||
lineWidth: 1,
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
});
|
||
|
||
bottomSeries.setData([
|
||
{ time: startTime, value: zd },
|
||
{ time: endTime, value: zd }
|
||
]);
|
||
|
||
// 添加左边界
|
||
const leftSeries = mainChart.addLineSeries({
|
||
color: '#F1C40F', // 主周期中枢颜色
|
||
lineWidth: 1,
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
});
|
||
|
||
leftSeries.setData([
|
||
{ time: startTime, value: zd },
|
||
{ time: startTime, value: zg }
|
||
]);
|
||
|
||
// 添加一个标记,标识这是未完成中枢
|
||
const markerSeries = mainChart.addLineSeries({
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
});
|
||
|
||
markerSeries.setMarkers([
|
||
{
|
||
time: startTime,
|
||
position: 'aboveBar',
|
||
color: '#F1C40F',
|
||
shape: 'circle',
|
||
text: '未完',
|
||
size: 1
|
||
}
|
||
]);
|
||
|
||
// 绘制gg线(中枢高高)
|
||
if (!isNaN(gg) && gg > 0) {
|
||
const ggSeries = mainChart.addLineSeries({
|
||
color: '#F1C40F', // 使用中枢自己的颜色
|
||
lineWidth: 1,
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
});
|
||
|
||
ggSeries.setData([
|
||
{ time: startTime, value: gg },
|
||
{ time: endTime, value: gg }
|
||
]);
|
||
}
|
||
|
||
// 绘制dd线(中枢低低)
|
||
if (!isNaN(dd) && dd > 0) {
|
||
const ddSeries = mainChart.addLineSeries({
|
||
color: '#F1C40F', // 使用中枢自己的颜色
|
||
lineWidth: 1,
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
});
|
||
|
||
ddSeries.setData([
|
||
{ time: startTime, value: dd },
|
||
{ time: endTime, value: dd }
|
||
]);
|
||
}
|
||
|
||
// 添加到图表对象
|
||
tvWidget.series.mainUncompletedZsSeries.push({
|
||
time: startTime,
|
||
value: zg,
|
||
color: '#F1C40F',
|
||
lineWidth: 1
|
||
});
|
||
tvWidget.series.mainUncompletedZsSeries.push({
|
||
time: endTime,
|
||
value: zg,
|
||
color: '#F1C40F',
|
||
lineWidth: 1
|
||
});
|
||
tvWidget.series.mainUncompletedZsSeries.push({
|
||
time: startTime,
|
||
value: zd,
|
||
color: '#F1C40F',
|
||
lineWidth: 1
|
||
});
|
||
tvWidget.series.mainUncompletedZsSeries.push({
|
||
time: endTime,
|
||
value: zd,
|
||
color: '#F1C40F',
|
||
lineWidth: 1
|
||
});
|
||
} catch (e) {
|
||
console.error('主周期未完成中枢处理出错:', e);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 次周期未完成中枢
|
||
if ($('#showElementZs').is(':checked') && currentData.element_uncompleted_zs_list && currentData.element_uncompleted_zs_list.length > 0) {
|
||
console.log(`绘制次周期未完成中枢数据,共${currentData.element_uncompleted_zs_list.length}条`);
|
||
|
||
currentData.element_uncompleted_zs_list.forEach(function(zs) {
|
||
try {
|
||
// 直接使用UTC时间戳(秒)
|
||
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
|
||
// 未完成中枢的结束时间设为当前K线的最后时间
|
||
const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
|
||
|
||
if (isNaN(startTime) || isNaN(endTime)) {
|
||
console.error('次周期未完成中枢时间转换错误:', zs.start_time);
|
||
return;
|
||
}
|
||
|
||
const zg = parseFloat(zs.zg); // 中枢上沿
|
||
const zd = parseFloat(zs.zd); // 中枢下沿
|
||
const gg = parseFloat(zs.gg); // 中枢高高
|
||
const dd = parseFloat(zs.dd); // 中枢低低
|
||
|
||
if (isNaN(zg) || isNaN(zd)) {
|
||
console.error('次周期未完成中枢价格转换错误:', zs.zg, zs.zd);
|
||
return;
|
||
}
|
||
|
||
// 创建未完成中枢上边界
|
||
const topSeries = mainChart.addLineSeries({
|
||
color: '#3f51b5', // 次周期中枢颜色
|
||
lineWidth: 1,
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
});
|
||
|
||
topSeries.setData([
|
||
{ time: startTime, value: zg },
|
||
{ time: endTime, value: zg }
|
||
]);
|
||
|
||
// 为下边界创建另一条线
|
||
const bottomSeries = mainChart.addLineSeries({
|
||
color: '#3f51b5', // 次周期中枢颜色
|
||
lineWidth: 1,
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
});
|
||
|
||
bottomSeries.setData([
|
||
{ time: startTime, value: zd },
|
||
{ time: endTime, value: zd }
|
||
]);
|
||
|
||
// 添加左边界
|
||
const leftSeries = mainChart.addLineSeries({
|
||
color: '#3f51b5', // 次周期中枢颜色
|
||
lineWidth: 1,
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
});
|
||
|
||
leftSeries.setData([
|
||
{ time: startTime, value: zd },
|
||
{ time: startTime, value: zg }
|
||
]);
|
||
|
||
// 添加一个标记,标识这是未完成中枢
|
||
const markerSeries = mainChart.addLineSeries({
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
});
|
||
|
||
markerSeries.setMarkers([
|
||
{
|
||
time: startTime,
|
||
position: 'aboveBar',
|
||
color: '#3f51b5',
|
||
shape: 'circle',
|
||
text: '未完',
|
||
size: 1
|
||
}
|
||
]);
|
||
|
||
// 绘制gg线(中枢高高)
|
||
if (!isNaN(gg) && gg > 0) {
|
||
const ggSeries = mainChart.addLineSeries({
|
||
color: '#3f51b5', // 使用次周期中枢自己的颜色
|
||
lineWidth: 1,
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
});
|
||
|
||
ggSeries.setData([
|
||
{ time: startTime, value: gg },
|
||
{ time: endTime, value: gg }
|
||
]);
|
||
}
|
||
|
||
// 绘制dd线(中枢低低)
|
||
if (!isNaN(dd) && dd > 0) {
|
||
const ddSeries = mainChart.addLineSeries({
|
||
color: '#3f51b5', // 使用次周期中枢自己的颜色
|
||
lineWidth: 1,
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
});
|
||
|
||
ddSeries.setData([
|
||
{ time: startTime, value: dd },
|
||
{ time: endTime, value: dd }
|
||
]);
|
||
}
|
||
|
||
// 添加到图表对象
|
||
tvWidget.series.elementUncompletedZsSeries.push({
|
||
time: startTime,
|
||
value: zg,
|
||
color: '#3f51b5',
|
||
lineWidth: 1
|
||
});
|
||
tvWidget.series.elementUncompletedZsSeries.push({
|
||
time: endTime,
|
||
value: zg,
|
||
color: '#3f51b5',
|
||
lineWidth: 1
|
||
});
|
||
tvWidget.series.elementUncompletedZsSeries.push({
|
||
time: startTime,
|
||
value: zd,
|
||
color: '#3f51b5',
|
||
lineWidth: 1
|
||
});
|
||
tvWidget.series.elementUncompletedZsSeries.push({
|
||
time: endTime,
|
||
value: zd,
|
||
color: '#3f51b5',
|
||
lineWidth: 1
|
||
});
|
||
} catch (e) {
|
||
console.error('次周期未完成中枢处理出错:', e);
|
||
}
|
||
});
|
||
}
|
||
// 次次周期未完成SEG中枢
|
||
if ($('#showSubSubZs').is(':checked') && currentData.sub_sub_uncompleted_zs_list && currentData.sub_sub_uncompleted_zs_list.length > 0) {
|
||
const kdZs = currentData.kline_data || [];
|
||
const endTimeZs = kdZs.length ? Math.floor(new Date(kdZs[kdZs.length-1].date).getTime() / 1000) : 0;
|
||
const subSubUZsColor = '#00897b';
|
||
currentData.sub_sub_uncompleted_zs_list.forEach(function(zs) {
|
||
try {
|
||
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
|
||
if (isNaN(startTime) || !endTimeZs) return;
|
||
const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd);
|
||
if (isNaN(zg) || isNaN(zd)) return;
|
||
mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zg }, { time: endTimeZs, value: zg }]);
|
||
mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: endTimeZs, value: zd }]);
|
||
mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
|
||
if (!isNaN(gg) && gg > 0) mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: gg }, { time: endTimeZs, value: gg }]);
|
||
if (!isNaN(dd) && dd > 0) mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: dd }, { time: endTimeZs, value: dd }]);
|
||
} catch (e) { console.error('次次周期未完成中枢处理出错:', e); }
|
||
});
|
||
}
|
||
} else {
|
||
console.log('绘制未完成中枢 - 已禁用');
|
||
}
|
||
// 未完成BI中枢 - 使用独立的BI开关
|
||
if ($('#showMainBiZs').is(':checked') && currentData.uncompleted_bi_zs_list && currentData.uncompleted_bi_zs_list.length > 0) {
|
||
try { console.log(`绘制主周期未完成BI中枢数据,共${currentData.uncompleted_bi_zs_list.length}条`); } catch (e) {}
|
||
currentData.uncompleted_bi_zs_list.forEach(function(zs) {
|
||
try {
|
||
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
|
||
const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
|
||
if (isNaN(startTime) || isNaN(endTime)) { return; }
|
||
const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd);
|
||
if (isNaN(zg) || isNaN(zd)) { return; }
|
||
const color = '#F1C40F';
|
||
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
|
||
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
|
||
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
|
||
if (!isNaN(gg) && gg > 0) {
|
||
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
|
||
}
|
||
if (!isNaN(dd) && dd > 0) {
|
||
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
|
||
}
|
||
} catch (e) { console.error('主周期未完成BI中枢处理出错:', e); }
|
||
});
|
||
}
|
||
// 次周期 未完成 BI 中枢
|
||
if ($('#showElementBiZs').is(':checked') && currentData.element_uncompleted_bi_zs_list && currentData.element_uncompleted_bi_zs_list.length > 0) {
|
||
try { console.log(`绘制次周期未完成BI中枢数据,共${currentData.element_uncompleted_bi_zs_list.length}条`); } catch (e) {}
|
||
currentData.element_uncompleted_bi_zs_list.forEach(function(zs) {
|
||
try {
|
||
const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000);
|
||
const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000);
|
||
if (isNaN(startTime) || isNaN(endTime)) { return; }
|
||
const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd);
|
||
if (isNaN(zg) || isNaN(zd)) { return; }
|
||
const color = '#3f51b5';
|
||
const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]);
|
||
const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]);
|
||
const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]);
|
||
if (!isNaN(gg) && gg > 0) {
|
||
const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]);
|
||
}
|
||
if (!isNaN(dd) && dd > 0) {
|
||
const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false });
|
||
ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]);
|
||
}
|
||
} catch (e) { console.error('次周期未完成BI中枢处理出错:', e); }
|
||
});
|
||
}
|
||
// 添加买卖点标记(新版:基于 bsp_list / element_bsp_list / sub_sub_bsp_list,按与 KLC 分型相同方式合并到主图标记)
|
||
if ($('#showMainBsp').is(':checked') || $('#showElementBsp').is(':checked') || $('#showSubSubBsp').is(':checked')) {
|
||
console.log('绘制买卖点(BSP) - 已启用');
|
||
|
||
// BSP 样式定义
|
||
const BSP_STYLE = {
|
||
'BSP1_BUY': { color: '#FF1744', text: 'B1', position: 'belowBar', size: 0.5 },
|
||
'BSP2_BUY': { color: '#F50057', text: 'B2', position: 'belowBar', size: 0.5 },
|
||
'BSP3_BUY': { color: '#D500F9', text: 'B3', position: 'belowBar', size: 0.5 },
|
||
'BSP1_SELL': { color: '#00E676', text: 'S1', position: 'aboveBar', size: 0.5 },
|
||
'BSP2_SELL': { color: '#00B0FF', text: 'S2', position: 'aboveBar', size: 0.5 },
|
||
'BSP3_SELL': { color: '#8B4513', text: 'S3', position: 'aboveBar', size: 0.5 },
|
||
'BSP4_BUY': { color: '#FF6D00', text: 'B4', position: 'belowBar', size: 0.5 },
|
||
'BSP4_SELL': { color: '#0091EA', text: 'S4', position: 'aboveBar', size: 0.5 },
|
||
};
|
||
|
||
const getBspStyleKey = (bsp) => {
|
||
// 统一 BSP key:
|
||
// - type 可能是 "BSP1"/"BSP2"/"BSP3",
|
||
// - 也可能是后端给的 "B1"/"B2"/"B3" 或 "S1"/"S2"/"S3"
|
||
// 最终都映射为 "BSP1_BUY" / "BSP1_SELL" 这类 key,方便复用现有样式定义
|
||
let type = (bsp.type || '').toUpperCase();
|
||
const dir = (bsp.dir || '').toUpperCase();
|
||
|
||
// 若是 "B1" / "B2" / "B3" 或 "S1" / "S2" / "S3" 形式,则提取数字并映射成 "BSP{n}"
|
||
const simpleMatch = type.match(/^([BS])(\d)$/);
|
||
if (simpleMatch) {
|
||
const n = simpleMatch[2]; // "1" / "2" / "3"
|
||
type = 'BSP' + n;
|
||
}
|
||
|
||
return type + '_' + dir;
|
||
};
|
||
|
||
// 收集所有 BSP 标记
|
||
const allBspMarkers = [];
|
||
|
||
// 主周期买卖点
|
||
// 兼容不同字段命名:优先使用 bsp_list,若不存在则尝试 bsp
|
||
const mainBspList = currentData.bsp_list || currentData.bsp || [];
|
||
// 调试:打印前几条主周期 BSP 的 key,方便排查样式不匹配问题
|
||
if (mainBspList.length > 0) {
|
||
console.log(
|
||
'主周期 BSP 示例 (前5条):',
|
||
mainBspList.slice(0, 5).map(b => ({
|
||
raw_type: b.type,
|
||
raw_dir: b.dir,
|
||
key: getBspStyleKey(b)
|
||
}))
|
||
);
|
||
}
|
||
if ($('#showMainBsp').is(':checked') && mainBspList.length > 0) {
|
||
console.log(`绘制主周期买卖点,共${mainBspList.length}条`);
|
||
mainBspList.forEach(function(bsp) {
|
||
try {
|
||
const ts = Math.floor(new Date(bsp.time).getTime() / 1000);
|
||
if (isNaN(ts)) return;
|
||
const key = getBspStyleKey(bsp);
|
||
const style = BSP_STYLE[key] || { color: '#999', shape: 'circle', text: '?', position: 'inBar' };
|
||
const sureText = bsp.is_sure ? '' : '?';
|
||
allBspMarkers.push({
|
||
time: ts,
|
||
position: style.position,
|
||
color: style.color,
|
||
shape: style.shape,
|
||
text: style.text + sureText,
|
||
size: 2
|
||
});
|
||
} catch (e) {
|
||
console.error('主周期BSP处理出错:', e);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 次周期买卖点
|
||
// 兼容不同字段命名:优先使用 element_bsp_list,若不存在则尝试 element_bsp
|
||
const elementBspList = currentData.element_bsp_list || currentData.element_bsp || [];
|
||
// 调试:打印前几条次周期 BSP 的 key
|
||
if (elementBspList.length > 0) {
|
||
console.log(
|
||
'次周期 BSP 示例 (前5条):',
|
||
elementBspList.slice(0, 5).map(b => ({
|
||
raw_type: b.type,
|
||
raw_dir: b.dir,
|
||
key: getBspStyleKey(b)
|
||
}))
|
||
);
|
||
}
|
||
if ($('#showElementBsp').is(':checked') && elementBspList.length > 0) {
|
||
console.log(`绘制次周期买卖点,共${elementBspList.length}条`);
|
||
elementBspList.forEach(function(bsp) {
|
||
try {
|
||
const ts = Math.floor(new Date(bsp.time).getTime() / 1000);
|
||
if (isNaN(ts)) return;
|
||
const key = getBspStyleKey(bsp);
|
||
const style = BSP_STYLE[key] || { color: '#999', shape: 'circle', text: '?', position: 'inBar' };
|
||
const sureText = bsp.is_sure ? '' : '?';
|
||
// 次周期使用稍小的标记和不同前缀以区分
|
||
allBspMarkers.push({
|
||
time: ts,
|
||
position: style.position,
|
||
color: style.color,
|
||
shape: style.shape,
|
||
text: 'e' + style.text + sureText,
|
||
size: 1
|
||
});
|
||
} catch (e) {
|
||
console.error('次周期BSP处理出错:', e);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 次次周期买卖点
|
||
const subSubBspList = currentData.sub_sub_bsp_list || [];
|
||
if ($('#showSubSubBsp').is(':checked') && subSubBspList.length > 0) {
|
||
subSubBspList.forEach(function(bsp) {
|
||
try {
|
||
const ts = Math.floor(new Date(bsp.time).getTime() / 1000);
|
||
if (isNaN(ts)) return;
|
||
const key = getBspStyleKey(bsp);
|
||
const style = BSP_STYLE[key] || { color: '#999', shape: 'circle', text: '?', position: 'inBar' };
|
||
const sureText = bsp.is_sure ? '' : '?';
|
||
allBspMarkers.push({
|
||
time: ts,
|
||
position: style.position,
|
||
color: '#00897b',
|
||
shape: style.shape,
|
||
text: 's' + (style.text || '?') + sureText,
|
||
size: 1
|
||
});
|
||
} catch (e) {
|
||
console.error('次次周期BSP处理出错:', e);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 将 BSP 标记挂到全局,后面与 KLC 分型等标记一起合并到主系列上
|
||
if (allBspMarkers.length > 0) {
|
||
// 按时间排序(lightweight-charts 要求标记按时间升序)
|
||
allBspMarkers.sort((a, b) => a.time - b.time);
|
||
window.bspMarkers = allBspMarkers;
|
||
console.log(`准备合并 ${allBspMarkers.length} 个BSP标记到主图标记中`);
|
||
} else {
|
||
window.bspMarkers = [];
|
||
}
|
||
} else {
|
||
// 关闭 BSP 显示时,清空全局 BSP 标记
|
||
window.bspMarkers = [];
|
||
}
|
||
|
||
// 第四类买卖点(B4/S4):中枢突破回抽后当根入场,位置同 B3/S3 但早 7~8 根。
|
||
// 与 BSP 分开收集,因为它数量远多于 B1/B2/B3,混在一个开关里图会糊掉。
|
||
if ($('#showMainFastBsp').is(':checked') || $('#showElementFastBsp').is(':checked') || $('#showSubSubFastBsp').is(':checked')) {
|
||
// 深色 = 区间套(大级别分型同向) + 中枢顺向推进都满足;浅色 = 未通过过滤
|
||
const FAST_BSP_STYLE = {
|
||
'BUY': { strong: '#FF6D00', weak: '#FFCC80', text: 'B4', position: 'belowBar' },
|
||
'SELL': { strong: '#0091EA', weak: '#81D4FA', text: 'S4', position: 'aboveBar' },
|
||
};
|
||
const onlyFiltered = ($('#fastBspFilterMode').val() || 'all') === 'filtered';
|
||
const allFastBspMarkers = [];
|
||
|
||
const collectFastBsp = function(list, prefix, label) {
|
||
(list || []).forEach(function(bsp) {
|
||
try {
|
||
const ts = Math.floor(new Date(bsp.time).getTime() / 1000);
|
||
if (isNaN(ts)) return;
|
||
const style = FAST_BSP_STYLE[(bsp.dir || '').toUpperCase()];
|
||
if (!style) return;
|
||
const passed = !!(bsp.htf_agree && bsp.ladder_ok);
|
||
if (onlyFiltered && !passed) return;
|
||
allFastBspMarkers.push({
|
||
time: ts,
|
||
position: style.position,
|
||
color: passed ? style.strong : style.weak,
|
||
text: prefix + (passed ? style.text : style.text.toLowerCase()),
|
||
size: passed ? 2 : 1
|
||
});
|
||
} catch (e) {
|
||
console.error(label + '第四类买卖点处理出错:', e);
|
||
}
|
||
});
|
||
};
|
||
|
||
if ($('#showMainFastBsp').is(':checked')) {
|
||
collectFastBsp(currentData.fast_bsp_list, '', '主周期');
|
||
}
|
||
if ($('#showElementFastBsp').is(':checked')) {
|
||
collectFastBsp(currentData.element_fast_bsp_list, 'e', '次周期');
|
||
}
|
||
if ($('#showSubSubFastBsp').is(':checked')) {
|
||
collectFastBsp(currentData.sub_sub_fast_bsp_list, 's', '次次周期');
|
||
}
|
||
|
||
allFastBspMarkers.sort((a, b) => a.time - b.time);
|
||
window.fastBspMarkers = allFastBspMarkers;
|
||
console.log(`绘制第四类买卖点,共${allFastBspMarkers.length}个标记(${onlyFiltered ? '仅过滤后' : '全部'})`);
|
||
} else {
|
||
window.fastBspMarkers = [];
|
||
}
|
||
|
||
// 添加买卖点标记(旧版,保留兼容)
|
||
// 这里为了与主面板上的「买卖点」开关保持一致,
|
||
// 同时响应顶部的 `#showMainBsp` 复选框
|
||
if ($('#showTradePoints').is(':checked') || $('#showMainBsp').is(':checked')) {
|
||
console.log('绘制买卖点 - 已启用(来源: showTradePoints / showMainBsp)');
|
||
|
||
// 优先使用小周期数据,如果不存在则使用主周期数据
|
||
const tradePointsData = currentData.element_trade_points || currentData.trade_points;
|
||
console.log(`绘制${currentData.element_trade_points ? '元素周期' : '主周期'}买卖点数据,共${tradePointsData ? tradePointsData.length : 0}条`);
|
||
|
||
// 调试信息 - 输出完整的买卖点数据
|
||
if (tradePointsData && tradePointsData.length > 0) {
|
||
console.log("买卖点数据样例:", tradePointsData[0]);
|
||
|
||
// 检查数据格式,如果time不是标准格式,进行格式化处理
|
||
const checkDataFormat = () => {
|
||
for (let i = 0; i < tradePointsData.length; i++) {
|
||
if (tradePointsData[i].time) {
|
||
// 确保时间是标准格式
|
||
try {
|
||
const timeValue = new Date(tradePointsData[i].time);
|
||
if (isNaN(timeValue.getTime())) {
|
||
console.error(`买卖点 #${i} 时间格式无效:`, tradePointsData[i].time);
|
||
}
|
||
} catch (e) {
|
||
console.error(`买卖点 #${i} 时间格式异常:`, e);
|
||
}
|
||
} else {
|
||
console.error(`买卖点 #${i} 缺少时间属性`);
|
||
}
|
||
}
|
||
};
|
||
|
||
// 执行格式检查
|
||
checkDataFormat();
|
||
|
||
// 对买卖点按时间排序,用于后续优化显示
|
||
const sortedPoints = [...tradePointsData].sort((a, b) => {
|
||
return new Date(a.time) - new Date(b.time);
|
||
});
|
||
|
||
// 记录已处理的时间点 - 按类型分开计数
|
||
const processedTimes = {};
|
||
|
||
// 创建买卖点标记系列
|
||
const buyMarkers = [];
|
||
const sellMarkers = [];
|
||
|
||
// 计数器,追踪成功和失败的处理次数
|
||
let successCount = 0;
|
||
let errorCount = 0;
|
||
|
||
sortedPoints.forEach(function(point, index) {
|
||
try {
|
||
// 检查所有必要的属性是否存在且有效
|
||
if (!point.time || !point.price || point.type === undefined) {
|
||
console.error(`买卖点 #${index} 数据不完整:`, point);
|
||
errorCount++;
|
||
return;
|
||
}
|
||
|
||
const time = Math.floor(new Date(point.time).getTime() / 1000);
|
||
const price = parseFloat(point.price);
|
||
const type = parseInt(point.type);
|
||
|
||
if (isNaN(time) || isNaN(price) || isNaN(type)) {
|
||
console.error(`买卖点 #${index} 数据格式错误:`,
|
||
{ time: isNaN(time), price: isNaN(price), type: isNaN(type) }, point);
|
||
errorCount++;
|
||
return;
|
||
}
|
||
|
||
// 获取买卖点样式
|
||
const style = TRADE_POINT_STYLE[type] || {
|
||
color: '#999999',
|
||
shape: 'circle',
|
||
text: '?',
|
||
size: 1
|
||
};
|
||
|
||
// 初始化该时间点的类型计数器
|
||
if (!processedTimes[time]) {
|
||
processedTimes[time] = {};
|
||
}
|
||
|
||
// 优化:检查是否有相同时间点和相同类型的标记,如果有,进行类型内的偏移
|
||
let stackIndex = 0;
|
||
if (processedTimes[time][type]) {
|
||
// 已经有相同时间和类型的标记,记录堆叠索引
|
||
stackIndex = processedTimes[time][type];
|
||
processedTimes[time][type]++;
|
||
} else {
|
||
// 第一次出现这个时间点的这个类型
|
||
processedTimes[time][type] = 1;
|
||
}
|
||
|
||
// 为不同类型的买卖点获取基础垂直偏移系数
|
||
const baseOffset = TRADE_POINT_OFFSET[type] || 0;
|
||
|
||
// 创建标记对象,包含额外的信息用于悬停提示
|
||
const marker = {
|
||
time: time,
|
||
position: 'inBar', // 改为在K线内部显示,不影响数据
|
||
color: style.color,
|
||
shape: style.shape,
|
||
text: style.text,
|
||
size: style.size,
|
||
// 记录堆叠索引
|
||
stackIndex: stackIndex,
|
||
// 添加悬停提示的数据
|
||
tooltip: `<span class="${type > 0 ? 'buy-point' : 'sell-point'}">${point.desc || (type > 0 ? '买点' : '卖点')}</span><br>
|
||
时间: ${formatTime(point.time)}<br>
|
||
价格: ${price.toFixed(2)}`,
|
||
// 额外添加基础类型偏移
|
||
baseOffset: baseOffset,
|
||
// 添加边框
|
||
borderColor: 'white',
|
||
borderWidth: 1,
|
||
// 添加价格偏移系数
|
||
pricePercentOffset: PRICE_PERCENT_OFFSET[type] || 0,
|
||
// 保存实际价格用于计算
|
||
price: price,
|
||
// 保存类型
|
||
type: type
|
||
};
|
||
|
||
// 区分买卖点
|
||
if (type > 0) {
|
||
buyMarkers.push(marker);
|
||
} else {
|
||
sellMarkers.push(marker);
|
||
}
|
||
|
||
successCount++;
|
||
} catch (e) {
|
||
console.error(`处理买卖点 #${index} 出错:`, e, point);
|
||
errorCount++;
|
||
}
|
||
});
|
||
|
||
console.log(`买卖点处理完成: 成功=${successCount}, 失败=${errorCount}, 买点=${buyMarkers.length}, 卖点=${sellMarkers.length}`);
|
||
|
||
// 分别添加买卖点标记
|
||
if (buyMarkers.length > 0) {
|
||
const buyMarkersSeries = mainChart.addLineSeries({
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
lineVisible: false,
|
||
color: 'transparent',
|
||
title: '买点'
|
||
});
|
||
|
||
// 使用主K线的收盘价作为基准数据,保证买点标记与价格在同一纵轴范围
|
||
if (Array.isArray(candles) && candles.length > 0) {
|
||
const baseData = candles.map(c => ({ time: c.time, value: c.close }));
|
||
buyMarkersSeries.setData(baseData);
|
||
} else {
|
||
// 兜底:至少一个数据点,避免报错
|
||
buyMarkersSeries.setData([{ time: buyMarkers[0].time, value: buyMarkers[0].price || 0 }]);
|
||
}
|
||
|
||
try {
|
||
// 设置买点标记:文字在价格上方,仅显示文字不显示形状
|
||
buyMarkersSeries.setMarkers(
|
||
buyMarkers.map(marker => {
|
||
// 使用实际价格位置,买点显示在K线上方
|
||
return {
|
||
...marker,
|
||
position: 'aboveBar', // 买点:价格上方
|
||
price: marker.price,
|
||
// 隐藏形状,仅保留文字
|
||
size: 0,
|
||
color: 'rgba(0, 0, 0, 0)'
|
||
};
|
||
})
|
||
);
|
||
console.log(`成功添加 ${buyMarkers.length} 个买点标记`);
|
||
} catch (e) {
|
||
console.error("设置买点标记时出错:", e);
|
||
}
|
||
}
|
||
|
||
if (sellMarkers.length > 0) {
|
||
const sellMarkersSeries = mainChart.addLineSeries({
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
lineVisible: false,
|
||
color: 'transparent',
|
||
title: '卖点'
|
||
});
|
||
|
||
// 使用主K线的收盘价作为基准数据,保证卖点标记与价格在同一纵轴范围
|
||
if (Array.isArray(candles) && candles.length > 0) {
|
||
const baseData = candles.map(c => ({ time: c.time, value: c.close }));
|
||
sellMarkersSeries.setData(baseData);
|
||
} else {
|
||
// 兜底:至少一个数据点,避免报错
|
||
sellMarkersSeries.setData([{ time: sellMarkers[0].time, value: sellMarkers[0].price || 0 }]);
|
||
}
|
||
|
||
try {
|
||
// 设置卖点标记:文字在价格下方,仅显示文字不显示形状
|
||
sellMarkersSeries.setMarkers(
|
||
sellMarkers.map(marker => {
|
||
// 使用实际价格位置,卖点显示在K线下方
|
||
return {
|
||
...marker,
|
||
position: 'belowBar', // 卖点:价格下方
|
||
price: marker.price,
|
||
// 隐藏形状,仅保留文字
|
||
size: 0,
|
||
color: 'rgba(0, 0, 0, 0)'
|
||
};
|
||
})
|
||
);
|
||
console.log(`成功添加 ${sellMarkers.length} 个卖点标记`);
|
||
} catch (e) {
|
||
console.error("设置卖点标记时出错:", e);
|
||
}
|
||
}
|
||
|
||
// 添加鼠标悬停事件显示提示
|
||
mainChart.subscribeCrosshairMove(param => {
|
||
// 十字线同步到其他图表 - 通过DOM元素绘制垂直线实现虚线延长效果
|
||
if (param.time && param.point && volumeChart) {
|
||
try {
|
||
// 清除之前的十字线标记
|
||
const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line');
|
||
existingVolumeLines.forEach(line => line.remove());
|
||
const existingAtrLines = document.querySelectorAll('.atr-crosshair-line');
|
||
existingAtrLines.forEach(line => line.remove());
|
||
const existingMacdLines = document.querySelectorAll('.macd-crosshair-line');
|
||
existingMacdLines.forEach(line => line.remove());
|
||
const existingChanMacdLines = document.querySelectorAll('.chanmacd-crosshair-line');
|
||
existingChanMacdLines.forEach(line => line.remove());
|
||
|
||
// 获取时间对应的坐标位置
|
||
const mainTimeCoordinate = mainChart.timeScale().timeToCoordinate(param.time);
|
||
if (mainTimeCoordinate !== null) {
|
||
// 获取主图容器的位置
|
||
const mainChartRect = mainChartContainer.getBoundingClientRect();
|
||
|
||
// 在交易量图上绘制垂直线
|
||
const volumeTimeCoordinate = volumeChart.timeScale().timeToCoordinate(param.time);
|
||
if (volumeTimeCoordinate !== null) {
|
||
const volumeChartRect = volumeChartContainer.getBoundingClientRect();
|
||
const volumeLine = document.createElement('div');
|
||
volumeLine.className = 'volume-crosshair-line';
|
||
volumeLine.style.position = 'fixed'; // 改为fixed定位
|
||
volumeLine.style.left = (volumeChartRect.left + volumeTimeCoordinate) + 'px';
|
||
volumeLine.style.top = volumeChartRect.top + 'px';
|
||
volumeLine.style.width = '1px';
|
||
volumeLine.style.height = volumeChartRect.height + 'px';
|
||
volumeLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
|
||
volumeLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)';
|
||
volumeLine.style.pointerEvents = 'none';
|
||
volumeLine.style.zIndex = '1000';
|
||
document.body.appendChild(volumeLine);
|
||
}
|
||
|
||
// 在ATR图上绘制垂直线
|
||
if (atrChart && atrChartContainer) {
|
||
const atrTimeCoordinate = atrChart.timeScale().timeToCoordinate(param.time);
|
||
if (atrTimeCoordinate !== null) {
|
||
const atrChartRect = atrChartContainer.getBoundingClientRect();
|
||
const atrLine = document.createElement('div');
|
||
atrLine.className = 'atr-crosshair-line';
|
||
atrLine.style.position = 'fixed'; // 改为fixed定位
|
||
atrLine.style.left = (atrChartRect.left + atrTimeCoordinate) + 'px';
|
||
atrLine.style.top = atrChartRect.top + 'px';
|
||
atrLine.style.width = '1px';
|
||
atrLine.style.height = atrChartRect.height + 'px';
|
||
atrLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
|
||
atrLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)';
|
||
atrLine.style.pointerEvents = 'none';
|
||
atrLine.style.zIndex = '1000';
|
||
document.body.appendChild(atrLine);
|
||
}
|
||
}
|
||
|
||
// 如果有MACD图,也在MACD图上绘制垂直线
|
||
if (showMacd && macdChart && macdChartContainer) {
|
||
const macdTimeCoordinate = macdChart.timeScale().timeToCoordinate(param.time);
|
||
if (macdTimeCoordinate !== null) {
|
||
const macdChartRect = macdChartContainer.getBoundingClientRect();
|
||
const macdLine = document.createElement('div');
|
||
macdLine.className = 'macd-crosshair-line';
|
||
macdLine.style.position = 'fixed'; // 改为fixed定位
|
||
macdLine.style.left = (macdChartRect.left + macdTimeCoordinate) + 'px';
|
||
macdLine.style.top = macdChartRect.top + 'px';
|
||
macdLine.style.width = '1px';
|
||
macdLine.style.height = macdChartRect.height + 'px';
|
||
macdLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
|
||
macdLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)';
|
||
macdLine.style.pointerEvents = 'none';
|
||
macdLine.style.zIndex = '1000';
|
||
document.body.appendChild(macdLine);
|
||
}
|
||
}
|
||
|
||
// 如果有ChanMACD图,也在ChanMACD图上绘制垂直线
|
||
if (showMacd && chanMacdChart && chanMacdChartContainer) {
|
||
const chanMacdTimeCoordinate = chanMacdChart.timeScale().timeToCoordinate(param.time);
|
||
if (chanMacdTimeCoordinate !== null) {
|
||
const chanMacdChartRect = chanMacdChartContainer.getBoundingClientRect();
|
||
console.log('ChanMACD图表位置:', {
|
||
left: chanMacdChartRect.left,
|
||
top: chanMacdChartRect.top,
|
||
width: chanMacdChartRect.width,
|
||
height: chanMacdChartRect.height,
|
||
timeCoordinate: chanMacdTimeCoordinate
|
||
});
|
||
const chanMacdLine = document.createElement('div');
|
||
chanMacdLine.className = 'chanmacd-crosshair-line';
|
||
chanMacdLine.style.position = 'fixed';
|
||
chanMacdLine.style.left = (chanMacdChartRect.left + chanMacdTimeCoordinate) + 'px';
|
||
chanMacdLine.style.top = chanMacdChartRect.top + 'px';
|
||
chanMacdLine.style.width = '1px';
|
||
chanMacdLine.style.height = chanMacdChartRect.height + 'px';
|
||
chanMacdLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
|
||
chanMacdLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)';
|
||
chanMacdLine.style.pointerEvents = 'none';
|
||
chanMacdLine.style.zIndex = '1000';
|
||
document.body.appendChild(chanMacdLine);
|
||
console.log('ChanMACD垂直线已创建,位置:', chanMacdLine.style.left, chanMacdLine.style.top);
|
||
} else {
|
||
console.log('ChanMACD时间坐标为空');
|
||
}
|
||
} else {
|
||
console.log('ChanMACD图表条件不满足:', {
|
||
showMacd: showMacd,
|
||
hasChanMacdChart: !!chanMacdChart,
|
||
hasChanMacdChartContainer: !!chanMacdChartContainer
|
||
});
|
||
}
|
||
|
||
|
||
}
|
||
} catch (e) {
|
||
console.debug('十字线同步出错:', e);
|
||
}
|
||
} else {
|
||
// 当十字线离开时,清除垂直线
|
||
try {
|
||
const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line');
|
||
existingVolumeLines.forEach(line => line.remove());
|
||
const existingAtrLines = document.querySelectorAll('.atr-crosshair-line');
|
||
existingAtrLines.forEach(line => line.remove());
|
||
const existingMacdLines = document.querySelectorAll('.macd-crosshair-line');
|
||
existingMacdLines.forEach(line => line.remove());
|
||
const existingChanMacdLines = document.querySelectorAll('.chanmacd-crosshair-line');
|
||
existingChanMacdLines.forEach(line => line.remove());
|
||
} catch (e) {
|
||
console.debug('清除十字线时出错:', e);
|
||
}
|
||
}
|
||
|
||
if (param.time && param.point) {
|
||
const timeStr = param.time;
|
||
const markers = [...buyMarkers, ...sellMarkers].filter(m => m.time === timeStr);
|
||
|
||
// 同时检查分型标记
|
||
const fxMarkers = (window.fxMarkers || []).filter(m => m.time === timeStr);
|
||
const allMarkers = [...markers, ...fxMarkers];
|
||
|
||
// 显示时区调试信息
|
||
if (window.debugMode) {
|
||
const timezone = $('#timezone').val();
|
||
const formattedTime = formatTimeWithTimezone(timeStr * 1000, timezone);
|
||
|
||
// 获取当前价格 - 通过param.seriesPrices获取
|
||
let priceInfo = '';
|
||
if (param.seriesPrices && param.seriesPrices.size > 0) {
|
||
// 依次从当前可能的主系列中获取价格
|
||
if (tvWidget.series.candleSeries && param.seriesPrices.get(tvWidget.series.candleSeries)) {
|
||
const price = param.seriesPrices.get(tvWidget.series.candleSeries);
|
||
priceInfo = `价格: ${price.toFixed(2)}`;
|
||
} else if (tvWidget.series.renkoSeries && param.seriesPrices.get(tvWidget.series.renkoSeries)) {
|
||
const price = param.seriesPrices.get(tvWidget.series.renkoSeries);
|
||
priceInfo = `价格: ${price.toFixed(2)}`;
|
||
} else if (tvWidget.series.heikinSeries && param.seriesPrices.get(tvWidget.series.heikinSeries)) {
|
||
const price = param.seriesPrices.get(tvWidget.series.heikinSeries);
|
||
priceInfo = `价格: ${price.toFixed(2)}`;
|
||
} else if (tvWidget.series.barSeries && param.seriesPrices.get(tvWidget.series.barSeries)) {
|
||
const price = param.seriesPrices.get(tvWidget.series.barSeries);
|
||
priceInfo = `价格: ${price.toFixed(2)}`;
|
||
} else if (tvWidget.series.lineSeries && param.seriesPrices.get(tvWidget.series.lineSeries)) {
|
||
const price = param.seriesPrices.get(tvWidget.series.lineSeries);
|
||
priceInfo = `价格: ${price.toFixed(2)}`;
|
||
} else if (tvWidget.series.areaSeries && param.seriesPrices.get(tvWidget.series.areaSeries)) {
|
||
const price = param.seriesPrices.get(tvWidget.series.areaSeries);
|
||
priceInfo = `价格: ${price.toFixed(2)}`;
|
||
} else if (tvWidget.series.baselineSeries && param.seriesPrices.get(tvWidget.series.baselineSeries)) {
|
||
const price = param.seriesPrices.get(tvWidget.series.baselineSeries);
|
||
priceInfo = `价格: ${price.toFixed(2)}`;
|
||
}
|
||
// 如果没有蜡烛图系列价格,尝试从区域图系列获取
|
||
else if (tvWidget.series.areaSeries && param.seriesPrices.get(tvWidget.series.areaSeries)) {
|
||
const price = param.seriesPrices.get(tvWidget.series.areaSeries);
|
||
priceInfo = `价格: ${price.toFixed(2)}`;
|
||
}
|
||
// 如果没有蜡烛图系列价格,尝试从基线图系列获取
|
||
else if (tvWidget.series.baselineSeries && param.seriesPrices.get(tvWidget.series.baselineSeries)) {
|
||
const price = param.seriesPrices.get(tvWidget.series.baselineSeries);
|
||
priceInfo = `价格: ${price.toFixed(2)}`;
|
||
}
|
||
}
|
||
|
||
// 仅记录最简短的调试信息
|
||
console.debug(`十字线: ${timeStr} -> ${formattedTime} (${timezone})`);
|
||
|
||
// 显示自定义时区工具提示,包含价格信息
|
||
crosshairTooltip.innerHTML = `<div style="font-weight:bold">时间: ${formattedTime}</div>` +
|
||
(priceInfo ? `<div>${priceInfo}</div>` : '');
|
||
crosshairTooltip.style.display = 'block';
|
||
crosshairTooltip.style.left = (param.point.x + 15) + 'px';
|
||
crosshairTooltip.style.top = (param.point.y - 30) + 'px';
|
||
}
|
||
|
||
if (allMarkers.length > 0) {
|
||
// 有买卖点或分型标记,显示自定义提示
|
||
const tooltips = allMarkers.map(m => m.tooltip).join('<br><hr style="margin: 5px 0;">');
|
||
tooltipElement.innerHTML = tooltips;
|
||
tooltipElement.style.display = 'block';
|
||
tooltipElement.style.left = (param.point.x + 15) + 'px';
|
||
tooltipElement.style.top = (param.point.y + 15) + 'px';
|
||
} else {
|
||
// 隐藏提示
|
||
tooltipElement.style.display = 'none';
|
||
}
|
||
} else {
|
||
// 隐藏提示
|
||
tooltipElement.style.display = 'none';
|
||
crosshairTooltip.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
// 处理图表缩放、平移等事件,隐藏提示
|
||
mainChart.timeScale().subscribeVisibleTimeRangeChange(() => {
|
||
tooltipElement.style.display = 'none';
|
||
crosshairTooltip.style.display = 'none';
|
||
});
|
||
}
|
||
} else {
|
||
console.log('绘制买卖点 - 已禁用');
|
||
}
|
||
// 绘制布林带
|
||
if ($('#showMainBollinger').is(':checked') || $('#showElementBollinger').is(':checked')) {
|
||
console.log('绘制布林带 - 已启用');
|
||
|
||
// 主周期布林带
|
||
if ($('#showMainBollinger').is(':checked') && currentData.bollinger && currentData.bollinger.upper && currentData.bollinger.lower && currentData.bollinger.middle) {
|
||
console.log(`绘制主周期布林带数据,共${currentData.bollinger.upper.length}条`);
|
||
|
||
// 准备布林带数据
|
||
const upperBandData = [];
|
||
const lowerBandData = [];
|
||
const middleBandData = [];
|
||
|
||
// 主周期布林带始终使用主周期K线数据作为时间源
|
||
const mainKlineData = currentData.kline_data;
|
||
|
||
for (let i = 0; i < mainKlineData.length && i < currentData.bollinger.upper.length; i++) {
|
||
const kline = mainKlineData[i];
|
||
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
|
||
|
||
// 只添加非0的有效数据点
|
||
if (currentData.bollinger.upper[i] && currentData.bollinger.upper[i] !== 0) {
|
||
upperBandData.push({
|
||
time: timestamp,
|
||
value: currentData.bollinger.upper[i]
|
||
});
|
||
}
|
||
|
||
if (currentData.bollinger.lower[i] && currentData.bollinger.lower[i] !== 0) {
|
||
lowerBandData.push({
|
||
time: timestamp,
|
||
value: currentData.bollinger.lower[i]
|
||
});
|
||
}
|
||
|
||
if (currentData.bollinger.middle[i] && currentData.bollinger.middle[i] !== 0) {
|
||
middleBandData.push({
|
||
time: timestamp,
|
||
value: currentData.bollinger.middle[i]
|
||
});
|
||
}
|
||
}
|
||
|
||
// 创建布林带上轨
|
||
const upperBandSeries = mainChart.addLineSeries({
|
||
color: '#2196F3',
|
||
lineWidth: 1,
|
||
lineStyle: 2, // 虚线
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
title: '布林上轨'
|
||
});
|
||
upperBandSeries.setData(upperBandData);
|
||
|
||
// 创建布林带下轨
|
||
const lowerBandSeries = mainChart.addLineSeries({
|
||
color: '#2196F3',
|
||
lineWidth: 1,
|
||
lineStyle: 2, // 虚线
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
title: '布林下轨'
|
||
});
|
||
lowerBandSeries.setData(lowerBandData);
|
||
|
||
// 创建布林带中轨(移动平均线)
|
||
const middleBandSeries = mainChart.addLineSeries({
|
||
color: '#FF9800',
|
||
lineWidth: 1,
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
title: '布林中轨'
|
||
});
|
||
middleBandSeries.setData(middleBandData);
|
||
|
||
// 保存到tvWidget.series对象
|
||
tvWidget.series.mainBollingerSeries.push(upperBandSeries);
|
||
tvWidget.series.mainBollingerSeries.push(lowerBandSeries);
|
||
tvWidget.series.mainBollingerSeries.push(middleBandSeries);
|
||
|
||
console.log('主周期布林带绘制完成');
|
||
}
|
||
|
||
// 次周期布林带
|
||
if ($('#showElementBollinger').is(':checked') && currentData.element_bollinger && currentData.element_bollinger.upper && currentData.element_bollinger.lower && currentData.element_bollinger.middle) {
|
||
console.log(`绘制次周期布林带数据,共${currentData.element_bollinger.upper.length}条`);
|
||
|
||
// 准备次周期布林带数据
|
||
const elementUpperBandData = [];
|
||
const elementLowerBandData = [];
|
||
const elementMiddleBandData = [];
|
||
|
||
// 使用次周期K线数据
|
||
const elementKlineData = currentData.element_kline_data || currentData.kline_data;
|
||
|
||
for (let i = 0; i < elementKlineData.length && i < currentData.element_bollinger.upper.length; i++) {
|
||
const kline = elementKlineData[i];
|
||
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
|
||
|
||
// 只添加非0的有效数据点
|
||
if (currentData.element_bollinger.upper[i] && currentData.element_bollinger.upper[i] !== 0) {
|
||
elementUpperBandData.push({
|
||
time: timestamp,
|
||
value: currentData.element_bollinger.upper[i]
|
||
});
|
||
}
|
||
|
||
if (currentData.element_bollinger.lower[i] && currentData.element_bollinger.lower[i] !== 0) {
|
||
elementLowerBandData.push({
|
||
time: timestamp,
|
||
value: currentData.element_bollinger.lower[i]
|
||
});
|
||
}
|
||
|
||
if (currentData.element_bollinger.middle[i] && currentData.element_bollinger.middle[i] !== 0) {
|
||
elementMiddleBandData.push({
|
||
time: timestamp,
|
||
value: currentData.element_bollinger.middle[i]
|
||
});
|
||
}
|
||
}
|
||
|
||
// 创建次周期布林带上轨
|
||
const elementUpperBandSeries = mainChart.addLineSeries({
|
||
color: '#9C27B0',
|
||
lineWidth: 1,
|
||
lineStyle: 2, // 虚线
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
title: '次周期布林上轨'
|
||
});
|
||
elementUpperBandSeries.setData(elementUpperBandData);
|
||
|
||
// 创建次周期布林带下轨
|
||
const elementLowerBandSeries = mainChart.addLineSeries({
|
||
color: '#9C27B0',
|
||
lineWidth: 1,
|
||
lineStyle: 2, // 虚线
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
title: '次周期布林下轨'
|
||
});
|
||
elementLowerBandSeries.setData(elementLowerBandData);
|
||
|
||
// 创建次周期布林带中轨
|
||
const elementMiddleBandSeries = mainChart.addLineSeries({
|
||
color: '#E91E63',
|
||
lineWidth: 1,
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
title: '次周期布林中轨'
|
||
});
|
||
elementMiddleBandSeries.setData(elementMiddleBandData);
|
||
|
||
// 保存到tvWidget.series对象
|
||
tvWidget.series.elementBollingerSeries.push(elementUpperBandSeries);
|
||
tvWidget.series.elementBollingerSeries.push(elementLowerBandSeries);
|
||
tvWidget.series.elementBollingerSeries.push(elementMiddleBandSeries);
|
||
|
||
console.log('次周期布林带绘制完成');
|
||
}
|
||
} else {
|
||
console.log('绘制布林带 - 已禁用');
|
||
}
|
||
|
||
// 绘制分型类型标签
|
||
console.log('=== 开始检查分型显示条件 ===');
|
||
console.log('showKlcFxType勾选状态:', $('#showKlcFxType').is(':checked'));
|
||
console.log('showKluFxType勾选状态:', $('#showKluFxType').is(':checked'));
|
||
console.log('currentData.klc_fx_info存在:', !!currentData.klc_fx_info);
|
||
console.log('currentData.klu_fx_info存在:', !!currentData.klu_fx_info);
|
||
console.log('currentData.klc_fx_info长度:', currentData.klc_fx_info ? currentData.klc_fx_info.length : 'undefined');
|
||
console.log('currentData.klu_fx_info长度:', currentData.klu_fx_info ? currentData.klu_fx_info.length : 'undefined');
|
||
if (currentData.klc_fx_info && currentData.klc_fx_info.length > 0) {
|
||
console.log('前3个klc分型数据样本:', currentData.klc_fx_info.slice(0, 3));
|
||
}
|
||
if (currentData.klu_fx_info && currentData.klu_fx_info.length > 0) {
|
||
console.log('前3个klu分型数据样本:', currentData.klu_fx_info.slice(0, 3));
|
||
}
|
||
// 收集所有主周期分型标记
|
||
const allMainFxMarkers = [];
|
||
const mainFxMarkers = []; // 用于tooltip支持
|
||
// 处理主周期KLC分型
|
||
if ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) {
|
||
console.log(`绘制主周期K线合并分型标签,共${currentData.klc_fx_info.length}条`);
|
||
|
||
currentData.klc_fx_info.forEach(function(fx) {
|
||
try {
|
||
// 直接使用UTC时间戳(秒)
|
||
const timestamp = Math.floor(new Date(fx.time).getTime() / 1000);
|
||
const price = parseFloat(fx.price);
|
||
|
||
if (isNaN(timestamp) || isNaN(price)) {
|
||
console.error('主周期KLC分型时间或价格转换错误:', fx.time, fx.price);
|
||
return;
|
||
}
|
||
// 主周期 KLC
|
||
// 确定颜色和位置
|
||
const color = fx.is_bottom ? '#28a745' : '#dc3545'; // 底分型绿色,顶分型红色
|
||
|
||
// 根据强度等级调整颜色强度
|
||
let strengthColor = color;
|
||
|
||
// 构建显示文本,包含分型类型和强度信息
|
||
let displayText = `${fx.fx_strength.toFixed(1)}`;
|
||
if (fx.fx_strength < 1.0) { // 降低阈值,让更多分型显示
|
||
displayText = fx.fx_strength >= 0.8 ? '' : '' // 0.8以上显示点,0.8以下不显示文本
|
||
}
|
||
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("31", "").replace("41", "").replace("51", "").replace("01", "");
|
||
// 添加标记配置
|
||
const markerConfig = {
|
||
time: timestamp,
|
||
position: fx.is_bottom ? 'belowBar' : 'aboveBar',
|
||
color: strengthColor,
|
||
shape: 'triangle',
|
||
text: displayText,
|
||
size: 2 // 调整尺寸,强分型稍大,普通分型更小
|
||
};
|
||
|
||
allMainFxMarkers.push(markerConfig);
|
||
|
||
// 画虚线分型框(根据 start/end + high/low)
|
||
if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) {
|
||
const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000);
|
||
const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000);
|
||
const high = parseFloat(fx.high);
|
||
const low = parseFloat(fx.low);
|
||
|
||
if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) {
|
||
const boxHigh = Math.max(high, low);
|
||
const boxLow = Math.min(high, low);
|
||
const boxColor = strengthColor;
|
||
|
||
const topSeries = mainChart.addLineSeries({
|
||
color: boxColor,
|
||
lineWidth: 1,
|
||
lineStyle: 2, // 虚线
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
crosshairMarkerVisible: false,
|
||
});
|
||
safeOverlayLineSetData(topSeries, [{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]);
|
||
|
||
const bottomSeries = mainChart.addLineSeries({
|
||
color: boxColor,
|
||
lineWidth: 1,
|
||
lineStyle: 2, // 虚线
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
crosshairMarkerVisible: false,
|
||
});
|
||
safeOverlayLineSetData(bottomSeries, [{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
|
||
|
||
pushFxBoxVertical(startTs, boxLow, boxHigh, boxColor);
|
||
pushFxBoxVertical(endTs, boxLow, boxHigh, boxColor);
|
||
|
||
if (!tvWidget.series.mainKlcFxBoxSeries) tvWidget.series.mainKlcFxBoxSeries = [];
|
||
tvWidget.series.mainKlcFxBoxSeries.push(topSeries, bottomSeries);
|
||
}
|
||
}
|
||
|
||
// 创建分型标记对象,包含tooltip信息
|
||
const fxMarker = {
|
||
time: timestamp,
|
||
tooltip: `<div style="color: ${strengthColor}; font-weight: bold;">
|
||
主周期${fx.is_bottom ? '底分型' : '顶分型'}(合): ${fx.fx_type}<br>
|
||
强度分数: ${fx.fx_strength}分<br>
|
||
强度等级: ${fx.fx_strength_level}<br>
|
||
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
|
||
价格: ${price.toFixed(4)}<br>
|
||
时间: ${fx.time}
|
||
</div>`
|
||
};
|
||
|
||
mainFxMarkers.push(fxMarker);
|
||
|
||
} catch (e) {
|
||
console.error('绘制主周期KLC分型标签出错:', e);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 处理主周期KLU分型
|
||
if ($('#showKluFxType').is(':checked') && currentData.klu_fx_info && currentData.klu_fx_info.length > 0) {
|
||
console.log(`绘制主周期K线未合并分型标签,共${currentData.klu_fx_info.length}条`);
|
||
|
||
currentData.klu_fx_info.forEach(function(fx) {
|
||
try {
|
||
// 直接使用UTC时间戳(秒)
|
||
const timestamp = Math.floor(new Date(fx.time).getTime() / 1000);
|
||
const price = parseFloat(fx.price);
|
||
|
||
if (isNaN(timestamp) || isNaN(price)) {
|
||
console.error('主周期KLU分型时间或价格转换错误:', fx.time, fx.price);
|
||
return;
|
||
}
|
||
|
||
// 主周期 KLU
|
||
const color = fx.is_bottom ? '#17a2b8' : '#fd7e14'; // 底分型用青色,顶分型用橙色
|
||
|
||
// 根据强度等级调整颜色强度
|
||
let strengthColor = color;
|
||
if (fx.is_strong_fx) {
|
||
// 强分型使用更亮的颜色
|
||
strengthColor = fx.is_bottom ? '#20c997' : '#fd7e14';
|
||
}
|
||
|
||
// 构建显示文本,包含分型类型和强度信息
|
||
let displayText = `${fx.fx_strength.toFixed(1)}`;
|
||
if (fx.fx_strength < 1.0) { // 降低阈值,让更多分型显示
|
||
displayText = fx.fx_strength >= 1.5 ? '' : '' // 0.8以上显示点,0.8以下不显示文本
|
||
}
|
||
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "");
|
||
// 添加标记配置
|
||
const markerConfig = {
|
||
time: timestamp,
|
||
position: fx.is_bottom ? 'belowBar' : 'aboveBar',
|
||
color: strengthColor,
|
||
// shape: 'triangle', // 使用三角形区分KLU分型
|
||
text: displayText,
|
||
size: fx.is_strong_fx ? 0.8 : 0.5 // KLU分型稍小一些
|
||
};
|
||
|
||
allMainFxMarkers.push(markerConfig);
|
||
|
||
// 创建分型标记对象,包含tooltip信息
|
||
const fxMarker = {
|
||
time: timestamp,
|
||
tooltip: `<div style="color: ${strengthColor}; font-weight: bold;">
|
||
主周期${fx.is_bottom ? '底分型' : '顶分型'}(原): ${fx.fx_type}<br>
|
||
强度分数: ${fx.fx_strength}分<br>
|
||
强度等级: ${fx.fx_strength_level}<br>
|
||
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
|
||
价格: ${price.toFixed(4)}<br>
|
||
时间: ${fx.time}
|
||
</div>`
|
||
};
|
||
|
||
mainFxMarkers.push(fxMarker);
|
||
|
||
} catch (e) {
|
||
console.error('绘制主周期KLU分型标签出错:', e);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 暂存主周期分型标记
|
||
window.mainFxMarkers = allMainFxMarkers;
|
||
|
||
// 将分型标记添加到全局markers中以支持tooltip功能
|
||
if (window.fxMarkers) {
|
||
window.fxMarkers = [...window.fxMarkers, ...mainFxMarkers];
|
||
} else {
|
||
window.fxMarkers = mainFxMarkers;
|
||
}
|
||
|
||
// 检查是否有任何主周期分型数据
|
||
const hasMainFxData = ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) ||
|
||
($('#showKluFxType').is(':checked') && currentData.klu_fx_info && currentData.klu_fx_info.length > 0);
|
||
|
||
if (!hasMainFxData) {
|
||
console.log('绘制主周期分型标记 - 已禁用或无数据');
|
||
// 清空主周期分型标记
|
||
window.mainFxMarkers = [];
|
||
window.fxMarkers = [];
|
||
}
|
||
window._areaTextLabels = alignMarkersToCandles(
|
||
buildAreaDivMarkersFromData(currentData).concat(buildAreaHistMarkersFromData(currentData)),
|
||
candles
|
||
);
|
||
if (typeof window._redrawFxBoxVerticalOverlay === 'function') {
|
||
window._redrawFxBoxVerticalOverlay();
|
||
}
|
||
|
||
// 绘制小周期分型标记(含次次周期)
|
||
if (($('#showElementKlcFxType').is(':checked') && currentData.element_klc_fx_info && currentData.element_klc_fx_info.length > 0) ||
|
||
($('#showElementKluFxType').is(':checked') && currentData.element_klu_fx_info && currentData.element_klu_fx_info.length > 0) ||
|
||
($('#showSubSubKlcFxType').is(':checked') && currentData.sub_sub_klc_fx_info && currentData.sub_sub_klc_fx_info.length > 0)) {
|
||
|
||
// 收集所有小周期分型标记
|
||
const allElementFxMarkers = [];
|
||
const elementFxMarkers = []; // 用于tooltip支持
|
||
|
||
// 处理小周期KLC分型
|
||
if ($('#showElementKlcFxType').is(':checked') && currentData.element_klc_fx_info && currentData.element_klc_fx_info.length > 0) {
|
||
console.log(`绘制小周期K线合并分型标记,共${currentData.element_klc_fx_info.length}条`);
|
||
|
||
currentData.element_klc_fx_info.forEach(function(fx) {
|
||
try {
|
||
// 直接使用UTC时间戳(秒)
|
||
const timestamp = Math.floor(new Date(fx.time).getTime() / 1000);
|
||
const price = parseFloat(fx.price);
|
||
|
||
if (isNaN(timestamp) || isNaN(price)) {
|
||
console.error('小周期KLC分型时间或价格转换错误:', fx.time, fx.price);
|
||
return;
|
||
}
|
||
|
||
// 小周期 KLC
|
||
let strengthColor = fx.is_bottom ? '#11116B' : '#222222'; // 底分型用珊瑚红,顶分型用薄荷绿
|
||
let displayText = `${fx.fx_strength.toFixed(1)}`;
|
||
// 构建小周期分型显示文本
|
||
if (fx.fx_strength < 1.0){ // 调整小周期阈值
|
||
displayText = fx.fx_strength >= 0.6 ? '' : '' // 0.6以上显示点
|
||
}
|
||
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("3", "").replace("41", "").replace("51", "").replace("0", "");
|
||
// 小周期分型标记配置
|
||
const markerConfig = {
|
||
time: timestamp,
|
||
position: fx.is_bottom ? 'belowBar' : 'aboveBar',
|
||
color: strengthColor,
|
||
shape: 'triangle',
|
||
text: displayText,
|
||
size: fx.is_strong_fx ? 0.8 : 0.6 // 小周期标记整体更小一些
|
||
};
|
||
|
||
allElementFxMarkers.push(markerConfig);
|
||
|
||
// 画虚线分型框(小周期)
|
||
if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) {
|
||
const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000);
|
||
const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000);
|
||
const high = parseFloat(fx.high);
|
||
const low = parseFloat(fx.low);
|
||
|
||
if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) {
|
||
const boxHigh = Math.max(high, low);
|
||
const boxLow = Math.min(high, low);
|
||
const boxColor = strengthColor;
|
||
|
||
const topSeries = mainChart.addLineSeries({
|
||
color: boxColor,
|
||
lineWidth: 1,
|
||
lineStyle: 2,
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
crosshairMarkerVisible: false,
|
||
});
|
||
safeOverlayLineSetData(topSeries, [{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]);
|
||
|
||
const bottomSeries = mainChart.addLineSeries({
|
||
color: boxColor,
|
||
lineWidth: 1,
|
||
lineStyle: 2,
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
crosshairMarkerVisible: false,
|
||
});
|
||
safeOverlayLineSetData(bottomSeries, [{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
|
||
|
||
pushFxBoxVertical(startTs, boxLow, boxHigh, boxColor);
|
||
pushFxBoxVertical(endTs, boxLow, boxHigh, boxColor);
|
||
|
||
if (!tvWidget.series.elementKlcFxBoxSeries) tvWidget.series.elementKlcFxBoxSeries = [];
|
||
tvWidget.series.elementKlcFxBoxSeries.push(topSeries, bottomSeries);
|
||
}
|
||
}
|
||
|
||
// 创建小周期分型标记对象,包含tooltip信息
|
||
const elementFxMarker = {
|
||
time: timestamp,
|
||
tooltip: `<div style="color: ${strengthColor}; font-weight: bold;">
|
||
小周期${fx.is_bottom ? '底分型' : '顶分型'}(合): ${fx.fx_type}<br>
|
||
强度分数: ${fx.fx_strength}分<br>
|
||
强度等级: ${fx.fx_strength_level}<br>
|
||
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
|
||
价格: ${price.toFixed(4)}<br>
|
||
时间: ${fx.time}
|
||
</div>`
|
||
};
|
||
|
||
elementFxMarkers.push(elementFxMarker);
|
||
|
||
} catch (e) {
|
||
console.error('绘制小周期KLC分型标记出错:', e);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 处理小周期KLU分型
|
||
if ($('#showElementKluFxType').is(':checked') && currentData.element_klu_fx_info && currentData.element_klu_fx_info.length > 0) {
|
||
console.log(`绘制小周期K线未合并分型标记,共${currentData.element_klu_fx_info.length}条`);
|
||
|
||
currentData.element_klu_fx_info.forEach(function(fx) {
|
||
try {
|
||
// 直接使用UTC时间戳(秒)
|
||
const timestamp = Math.floor(new Date(fx.time).getTime() / 1000);
|
||
const price = parseFloat(fx.price);
|
||
|
||
if (isNaN(timestamp) || isNaN(price)) {
|
||
console.error('小周期KLU分型时间或价格转换错误:', fx.time, fx.price);
|
||
return;
|
||
}
|
||
|
||
// 小周期 KLU
|
||
let strengthColor = fx.is_bottom ? '#9A8C98' : '#F2CC8F'; // 底分型用灰紫色,顶分型用浅黄色
|
||
let displayText = `${fx.fx_strength.toFixed(1)}`;
|
||
// 构建小周期分型显示文本
|
||
if (fx.fx_strength < 2.0){ // 调整小周期阈值
|
||
displayText = fx.fx_strength >= 1.5 ? '' : '' // 0.6以上显示点
|
||
}
|
||
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "");
|
||
// 小周期KLU分型标记配置
|
||
const markerConfig = {
|
||
time: timestamp,
|
||
position: fx.is_bottom ? 'belowBar' : 'aboveBar',
|
||
color: strengthColor,
|
||
// shape: 'triangle', // 使用三角形区分KLU分型
|
||
text: displayText,
|
||
size: fx.is_strong_fx ? 0.7 : 0.5 // 小周期KLU标记更小一些
|
||
};
|
||
|
||
allElementFxMarkers.push(markerConfig);
|
||
|
||
// 创建小周期分型标记对象,包含tooltip信息
|
||
const elementFxMarker = {
|
||
time: timestamp,
|
||
tooltip: `<div style="color: ${strengthColor}; font-weight: bold;">
|
||
小周期${fx.is_bottom ? '底分型' : '顶分型'}(原): ${fx.fx_type}<br>
|
||
强度分数: ${fx.fx_strength}分<br>
|
||
强度等级: ${fx.fx_strength_level}<br>
|
||
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
|
||
价格: ${price.toFixed(4)}<br>
|
||
时间: ${fx.time}
|
||
</div>`
|
||
};
|
||
|
||
elementFxMarkers.push(elementFxMarker);
|
||
|
||
} catch (e) {
|
||
console.error('绘制小周期KLU分型标记出错:', e);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 次次周期KLC分型
|
||
if ($('#showSubSubKlcFxType').is(':checked') && currentData.sub_sub_klc_fx_info && currentData.sub_sub_klc_fx_info.length > 0) {
|
||
currentData.sub_sub_klc_fx_info.forEach(function(fx) {
|
||
try {
|
||
const timestamp = Math.floor(new Date(fx.time).getTime() / 1000);
|
||
const price = parseFloat(fx.price);
|
||
if (isNaN(timestamp) || isNaN(price)) return;
|
||
const strengthColor = '#00897b';
|
||
let displayText = (fx.fx_type || '').replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("3", "").replace("41", "").replace("51", "").replace("0", "");
|
||
const markerConfig = {
|
||
time: timestamp,
|
||
position: fx.is_bottom ? 'belowBar' : 'aboveBar',
|
||
color: strengthColor,
|
||
shape: 'triangle',
|
||
text: displayText,
|
||
size: (fx.is_strong_fx ? 0.6 : 0.5)
|
||
};
|
||
allElementFxMarkers.push(markerConfig);
|
||
|
||
// 画虚线分型框(次次周期)
|
||
if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) {
|
||
const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000);
|
||
const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000);
|
||
const high = parseFloat(fx.high);
|
||
const low = parseFloat(fx.low);
|
||
|
||
if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) {
|
||
const boxHigh = Math.max(high, low);
|
||
const boxLow = Math.min(high, low);
|
||
const boxColor = strengthColor;
|
||
|
||
const topSeries = mainChart.addLineSeries({
|
||
color: boxColor,
|
||
lineWidth: 1,
|
||
lineStyle: 2,
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
crosshairMarkerVisible: false,
|
||
});
|
||
safeOverlayLineSetData(topSeries, [{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]);
|
||
|
||
const bottomSeries = mainChart.addLineSeries({
|
||
color: boxColor,
|
||
lineWidth: 1,
|
||
lineStyle: 2,
|
||
lastValueVisible: false,
|
||
priceLineVisible: false,
|
||
crosshairMarkerVisible: false,
|
||
});
|
||
safeOverlayLineSetData(bottomSeries, [{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
|
||
|
||
pushFxBoxVertical(startTs, boxLow, boxHigh, boxColor);
|
||
pushFxBoxVertical(endTs, boxLow, boxHigh, boxColor);
|
||
|
||
if (!tvWidget.series.subSubKlcFxBoxSeries) tvWidget.series.subSubKlcFxBoxSeries = [];
|
||
tvWidget.series.subSubKlcFxBoxSeries.push(topSeries, bottomSeries);
|
||
}
|
||
}
|
||
} catch (e) { console.error('绘制次次周期KLC分型标记出错:', e); }
|
||
});
|
||
}
|
||
|
||
// 将小周期分型标记添加到全局markers中以支持tooltip功能
|
||
if (window.fxMarkers) {
|
||
window.fxMarkers = [...window.fxMarkers, ...elementFxMarkers];
|
||
} else {
|
||
window.fxMarkers = elementFxMarkers;
|
||
}
|
||
|
||
// 基于后端提供的 KLC 趋势生成标记(不进行任何计算)
|
||
let klcTrendMarkers = [];
|
||
try {
|
||
if (currentData.klc_trend && currentData.klc_trend.length > 0) {
|
||
console.log('KLC趋势点数量:', currentData.klc_trend.length, currentData.klc_trend.slice(0, 3));
|
||
// 当前图表的bar时间集合(秒)用于对齐标记到最近的K线
|
||
const seriesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set();
|
||
const nearestTime = (target) => {
|
||
if (!Array.isArray(candles) || candles.length === 0) return target;
|
||
// 简单线性查找(数据量通常可接受),必要时可替换为二分
|
||
let best = candles[0].time;
|
||
let bestDiff = Math.abs(best - target);
|
||
for (let i = 1; i < candles.length; i++) {
|
||
const t = candles[i].time;
|
||
const d = Math.abs(t - target);
|
||
if (d < bestDiff) { best = t; bestDiff = d; }
|
||
}
|
||
return best;
|
||
};
|
||
|
||
klcTrendMarkers = currentData.klc_trend.map(t => {
|
||
const ts = Math.floor(new Date(t.time).getTime() / 1000);
|
||
let timeAligned = seriesTimes.has(ts) ? ts : nearestTime(ts);
|
||
return buildKlcTrendMarker(timeAligned, t.trend, MAIN_KLC_TREND_STYLE);
|
||
});
|
||
console.log('KLC趋势标记(对齐后)示例:', klcTrendMarkers.slice(0, 5));
|
||
}
|
||
} catch (e) {
|
||
klcTrendMarkers = [];
|
||
}
|
||
// 暴露到全局以便调试或后续合并
|
||
window.klcTrendMarkers = klcTrendMarkers;
|
||
|
||
// 无论当前显示主/小周期,只要勾选对应Trend,就叠加出来
|
||
let trendMarkersToUse = [];
|
||
if ($('#showMainTrend').is(':checked')) {
|
||
trendMarkersToUse = trendMarkersToUse.concat(window.klcTrendMarkers || []);
|
||
}
|
||
if ($('#showElementTrend').is(':checked') && currentData.element_klc_trend) {
|
||
const candlesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set();
|
||
const nearestTime = (target) => {
|
||
if (!Array.isArray(candles) || candles.length === 0) return target;
|
||
let best = candles[0].time, bestDiff = Math.abs(best - target);
|
||
for (let i = 1; i < candles.length; i++) {
|
||
const t = candles[i].time, d = Math.abs(t - target);
|
||
if (d < bestDiff) { best = t; bestDiff = d; }
|
||
}
|
||
return best;
|
||
};
|
||
const elementMarkers = currentData.element_klc_trend.map(t => {
|
||
const ts = Math.floor(new Date(t.time).getTime() / 1000);
|
||
const timeAligned = candlesTimes.has(ts) ? ts : nearestTime(ts);
|
||
return buildKlcTrendMarker(timeAligned, t.trend, ELEMENT_KLC_TREND_STYLE);
|
||
});
|
||
trendMarkersToUse = trendMarkersToUse.concat(elementMarkers);
|
||
}
|
||
if ($('#showSubSubTrend').is(':checked') && currentData.sub_sub_klc_trend && currentData.sub_sub_klc_trend.length > 0) {
|
||
const candlesTimesSs = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set();
|
||
const nearestTimeSs = (target) => {
|
||
if (!Array.isArray(candles) || candles.length === 0) return target;
|
||
let best = candles[0].time, bestDiff = Math.abs(best - target);
|
||
for (let i = 1; i < candles.length; i++) {
|
||
const t = candles[i].time, d = Math.abs(t - target);
|
||
if (d < bestDiff) { best = t; bestDiff = d; }
|
||
}
|
||
return best;
|
||
};
|
||
const subSubMarkers = currentData.sub_sub_klc_trend.map(t => {
|
||
const ts = Math.floor(new Date(t.time).getTime() / 1000);
|
||
const timeAligned = candlesTimesSs.has(ts) ? ts : nearestTimeSs(ts);
|
||
return buildKlcTrendMarker(timeAligned, t.trend, SUB_SUB_KLC_TREND_STYLE);
|
||
});
|
||
trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers);
|
||
}
|
||
|
||
// 合并标记并设置(主图不画 U/穿零轴,只留背驰 SD/CD)
|
||
const combinedMarkers = [
|
||
...(window.mainFxMarkers || []),
|
||
...allElementFxMarkers,
|
||
...(window.kluDivMarkersMain || []),
|
||
...(window.kluDivMarkersElement || []),
|
||
...(window.kluDivMarkersSubSub || []),
|
||
...trendMarkersToUse,
|
||
...(window.bspMarkers || []),
|
||
...(window.fastBspMarkers || [])
|
||
];
|
||
if (combinedMarkers.length > 0) {
|
||
console.log(
|
||
'合并设置', combinedMarkers.length, '个标记(主周期分型:',
|
||
(window.mainFxMarkers || []).length,
|
||
'个,小周期分型:', allElementFxMarkers.length,
|
||
'个,背驰:', (window.kluDivMarkersMain || []).length,
|
||
'个,BSP标记:', (window.bspMarkers || []).length,
|
||
'个,第四类标记:', (window.fastBspMarkers || []).length,
|
||
'个)'
|
||
);
|
||
|
||
// 根据当前主系列类型设置标记
|
||
const klineType = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line'));
|
||
let targetSeries = null;
|
||
if (klineType === 'candlestick') targetSeries = tvWidget.series.candleSeries;
|
||
else if (klineType === 'renko') targetSeries = tvWidget.series.renkoSeries;
|
||
else if (klineType === 'heikin') targetSeries = tvWidget.series.heikinSeries;
|
||
else if (klineType === 'bar') targetSeries = tvWidget.series.barSeries;
|
||
else if (klineType === 'line') targetSeries = tvWidget.series.lineSeries;
|
||
else if (klineType === 'area') targetSeries = tvWidget.series.areaSeries;
|
||
else if (klineType === 'baseline') targetSeries = tvWidget.series.baselineSeries;
|
||
else if (klineType === 'klc') targetSeries = tvWidget.series.klcSeries;
|
||
if (targetSeries) {
|
||
try {
|
||
targetSeries.setMarkers(alignMarkersToCandles(combinedMarkers, candles));
|
||
} catch (e) {
|
||
console.warn('设置主系列标记失败(可能series已释放):', e);
|
||
}
|
||
} else {
|
||
console.log('未找到主数据系列,无法设置标记');
|
||
}
|
||
}
|
||
|
||
} else {
|
||
console.log('绘制小周期分型标记 - 已禁用或无数据');
|
||
|
||
// 计算并缓存KLC趋势标记(即使未启用小周期分型,也应显示趋势)
|
||
try {
|
||
let klcTrendMarkers = [];
|
||
if (currentData.klc_trend && currentData.klc_trend.length > 0) {
|
||
console.log('KLC趋势点数量:', currentData.klc_trend.length, currentData.klc_trend.slice(0, 3));
|
||
const seriesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set();
|
||
const nearestTime = (target) => {
|
||
if (!Array.isArray(candles) || candles.length === 0) return target;
|
||
let best = candles[0].time;
|
||
let bestDiff = Math.abs(best - target);
|
||
for (let i = 1; i < candles.length; i++) {
|
||
const t = candles[i].time;
|
||
const d = Math.abs(t - target);
|
||
if (d < bestDiff) { best = t; bestDiff = d; }
|
||
}
|
||
return best;
|
||
};
|
||
klcTrendMarkers = currentData.klc_trend.map(t => {
|
||
const ts = Math.floor(new Date(t.time).getTime() / 1000);
|
||
const timeAligned = seriesTimes.has(ts) ? ts : nearestTime(ts);
|
||
return buildKlcTrendMarker(timeAligned, t.trend, MAIN_KLC_TREND_STYLE);
|
||
});
|
||
console.log('KLC趋势标记(对齐后)示例:', klcTrendMarkers.slice(0, 5));
|
||
}
|
||
window.klcTrendMarkers = klcTrendMarkers;
|
||
} catch (e) {
|
||
window.klcTrendMarkers = [];
|
||
}
|
||
|
||
// 与上方一致:勾选哪个Trend就显示哪个
|
||
let trendMarkersToUse = [];
|
||
if ($('#showMainTrend').is(':checked')) {
|
||
trendMarkersToUse = trendMarkersToUse.concat(window.klcTrendMarkers || []);
|
||
}
|
||
if ($('#showElementTrend').is(':checked') && currentData.element_klc_trend) {
|
||
const candlesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set();
|
||
const nearestTime = (target) => {
|
||
if (!Array.isArray(candles) || candles.length === 0) return target;
|
||
let best = candles[0].time, bestDiff = Math.abs(best - target);
|
||
for (let i = 1; i < candles.length; i++) {
|
||
const t = candles[i].time, d = Math.abs(t - target);
|
||
if (d < bestDiff) { best = t; bestDiff = d; }
|
||
}
|
||
return best;
|
||
};
|
||
const elementMarkers = currentData.element_klc_trend.map(t => {
|
||
const ts = Math.floor(new Date(t.time).getTime() / 1000);
|
||
const timeAligned = candlesTimes.has(ts) ? ts : nearestTime(ts);
|
||
return buildKlcTrendMarker(timeAligned, t.trend, ELEMENT_KLC_TREND_STYLE);
|
||
});
|
||
trendMarkersToUse = trendMarkersToUse.concat(elementMarkers);
|
||
}
|
||
if ($('#showSubSubTrend').is(':checked') && currentData.sub_sub_klc_trend && currentData.sub_sub_klc_trend.length > 0) {
|
||
const candlesTimesSs2 = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set();
|
||
const nearestTimeSs2 = (target) => {
|
||
if (!Array.isArray(candles) || candles.length === 0) return target;
|
||
let best = candles[0].time, bestDiff = Math.abs(best - target);
|
||
for (let i = 1; i < candles.length; i++) {
|
||
const t = candles[i].time, d = Math.abs(t - target);
|
||
if (d < bestDiff) { best = t; bestDiff = d; }
|
||
}
|
||
return best;
|
||
};
|
||
const subSubMarkers2 = currentData.sub_sub_klc_trend.map(t => {
|
||
const ts = Math.floor(new Date(t.time).getTime() / 1000);
|
||
const timeAligned = candlesTimesSs2.has(ts) ? ts : nearestTimeSs2(ts);
|
||
return buildKlcTrendMarker(timeAligned, t.trend, SUB_SUB_KLC_TREND_STYLE);
|
||
});
|
||
trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers2);
|
||
}
|
||
// 这里的 onlyMainAndU 实际上是「最终要挂到主K线上」的一组标记
|
||
// 之前没有把 window.bspMarkers 合进去,导致上面已经合并了 BSP 标记,
|
||
// 但在这里再次调用 setMarkers 时把 BSP 覆盖掉了,从而前端看不到买卖点。
|
||
// 修复:把 BSP 标记一并合并进来。第四类买卖点同理,两处都要带上。
|
||
const onlyMainAndU = [
|
||
...(window.mainFxMarkers || []),
|
||
...(window.kluDivMarkersMain || []),
|
||
...(window.kluDivMarkersElement || []),
|
||
...(window.kluDivMarkersSubSub || []),
|
||
...trendMarkersToUse,
|
||
...(window.bspMarkers || []),
|
||
...(window.fastBspMarkers || [])
|
||
];
|
||
if (onlyMainAndU.length > 0) {
|
||
console.log('仅设置', onlyMainAndU.length, '个主图标记(主周期分型:', (window.mainFxMarkers || []).length, ',背驰:', (window.kluDivMarkersMain || []).length, ')');
|
||
|
||
// 根据当前主系列类型设置标记
|
||
const klineType2 = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line'));
|
||
let targetSeries2 = null;
|
||
if (klineType2 === 'candlestick') targetSeries2 = tvWidget.series.candleSeries;
|
||
else if (klineType2 === 'renko') targetSeries2 = tvWidget.series.renkoSeries;
|
||
else if (klineType2 === 'heikin') targetSeries2 = tvWidget.series.heikinSeries;
|
||
else if (klineType2 === 'bar') targetSeries2 = tvWidget.series.barSeries;
|
||
else if (klineType2 === 'line') targetSeries2 = tvWidget.series.lineSeries;
|
||
else if (klineType2 === 'area') targetSeries2 = tvWidget.series.areaSeries;
|
||
else if (klineType2 === 'baseline') targetSeries2 = tvWidget.series.baselineSeries;
|
||
else if (klineType2 === 'klc') targetSeries2 = tvWidget.series.klcSeries;
|
||
if (targetSeries2) {
|
||
try {
|
||
targetSeries2.setMarkers(alignMarkersToCandles(onlyMainAndU, candles));
|
||
} catch (e) {
|
||
console.warn('设置主系列标记失败(可能series已释放):', e);
|
||
}
|
||
} else {
|
||
console.log('未找到主数据系列,无法设置标记');
|
||
}
|
||
} else {
|
||
console.log('没有分型标记需要显示,清空图表标记');
|
||
// 清空图表上的所有主系列标记
|
||
const klineType3 = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line'));
|
||
let targetSeries3 = null;
|
||
if (klineType3 === 'candlestick') targetSeries3 = tvWidget.series.candleSeries;
|
||
else if (klineType3 === 'renko') targetSeries3 = tvWidget.series.renkoSeries;
|
||
else if (klineType3 === 'heikin') targetSeries3 = tvWidget.series.heikinSeries;
|
||
else if (klineType3 === 'bar') targetSeries3 = tvWidget.series.barSeries;
|
||
else if (klineType3 === 'line') targetSeries3 = tvWidget.series.lineSeries;
|
||
else if (klineType3 === 'area') targetSeries3 = tvWidget.series.areaSeries;
|
||
else if (klineType3 === 'baseline') targetSeries3 = tvWidget.series.baselineSeries;
|
||
else if (klineType3 === 'klc') targetSeries3 = tvWidget.series.klcSeries;
|
||
if (targetSeries3) {
|
||
try {
|
||
targetSeries3.setMarkers([]);
|
||
} catch (e) {
|
||
console.warn('清空主系列标记失败(可能series已释放):', e);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// KLC 分型框竖边:canvas 真竖线(LWC 折线做不到不斜)
|
||
try {
|
||
syncFxBoxVerticalOverlay(mainChart, mainChartContainer);
|
||
} catch (e) {
|
||
console.warn('分型竖边 overlay 失败:', e);
|
||
}
|
||
}
|