Files
Chan/web/static/js/app/chart_tv_overlays.js
T
jackyu66gitandCursor 90499533fb fix(web): 分析/自动刷新后保留 K 线视窗位置
拆分手动分析与自动刷新拉数路径;全量重建用 logical 优先恢复视窗,
增量 recent 用 scroll+barDelta;避免 barSpacing 重锚与重复冻结导致往右跳。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 23:38:20 +08:00

2507 lines
148 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* chart_tv_overlays.js — structure zones / wyckoff / 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;
}
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 series = getMainPriceSeries();
if (!series || !boxes.length) return '0';
var ts = mainChart.timeScale();
var a = boxes[0];
var b = boxes[boxes.length - 1];
return [
boxes.length,
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))
].join('|');
};
var redraw = function () {
var boxes = window._fxBoxVerticals || [];
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) {
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([]);
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 = [];
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); }
}
// 区间/阶段/时间/VP:框线同中枢;标记并入主 K(同 BSP),时间对齐 candles
(function drawWrLikeChan() {
const candleSeries = tvWidget.series.candleSeries
|| tvWidget.series.barSeries
|| tvWidget.series.lineSeries
|| tvWidget.series.areaSeries
|| tvWidget.series.heikinSeries
|| tvWidget.series.renkoSeries;
const candleTimes = (candles || []).map(function(c) { return c.time; })
.filter(function(t) { return t != null && !isNaN(t); });
const barStep = (candleTimes.length >= 2)
? Math.max(1, candleTimes[1] - candleTimes[0])
: 3600;
const lastKlineTime = candleTimes.length ? candleTimes[candleTimes.length - 1] : NaN;
const chartStart = candleTimes.length ? candleTimes[0] : NaN;
const toSec = function(t) {
if (t == null) return NaN;
if (typeof t === 'number') return t > 1e12 ? Math.floor(t / 1000) : Math.floor(t);
const ms = new Date(t).getTime();
return isNaN(ms) ? NaN : Math.floor(ms / 1000);
};
// 标记必须落在主 series 的 time 上(与 KLC 趋势 nearestTime 同思路)
const snapToCandle = function(t) {
if (!candleTimes.length || isNaN(t)) return t;
let best = candleTimes[0], bd = Math.abs(candleTimes[0] - t);
for (let i = 1; i < candleTimes.length; i++) {
const d = Math.abs(candleTimes[i] - t);
if (d < bd) { bd = d; best = candleTimes[i]; }
}
return best;
};
const ensureSpan = function(t0, t1) {
if (isNaN(t0) || isNaN(t1)) return [t0, t1];
if (t1 < t0) { const x = t0; t0 = t1; t1 = x; }
if (t1 <= t0) t1 = t0 + barStep;
return [t0, t1];
};
const drawBox = function(t0, t1, hi, lo, color) {
const span = ensureSpan(t0, t1);
t0 = span[0]; t1 = span[1];
if (isNaN(t0) || isNaN(t1) || isNaN(hi) || isNaN(lo)) return;
const opt = { color: color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false };
mainChart.addLineSeries(opt).setData([{ time: t0, value: hi }, { time: t1, value: hi }]);
mainChart.addLineSeries(opt).setData([{ time: t0, value: lo }, { time: t1, value: lo }]);
mainChart.addLineSeries(opt).setData([{ time: t0, value: lo }, { time: t0, value: hi }]);
mainChart.addLineSeries(opt).setData([{ time: t1, value: lo }, { time: t1, value: hi }]);
};
const mkPL = function(price, color, title, style) {
if (!candleSeries) return;
const p = parseFloat(price);
if (isNaN(p)) return;
try {
candleSeries.createPriceLine({
price: p, color: color,
lineWidth: style === 2 ? 1 : 2,
lineStyle: style || 0,
axisLabelVisible: true,
title: title
});
} catch (e) { console.warn('价位线失败', title, e); }
};
const phaseColors = { A: '#f1c40f', B: '#9b59b6', C: '#e67e22', D: '#2ecc71', E: '#3498db' };
const eventColors = {
Spring: '#27ae60', SOS: '#2ecc71', LPS: '#16a085',
UTAD: '#e74c3c', SOW: '#c0392b', LPSY: '#d35400'
};
const wrMarkers = [];
window.wrMarkers = [];
const pushMarker = function(m) {
if (!m || isNaN(m.time)) return;
m.time = snapToCandle(m.time);
if (!isNaN(chartStart) && (m.time < chartStart || m.time > lastKlineTime)) return;
wrMarkers.push(m);
};
const drawOne = function(w, cfg) {
if (!w) return;
const showR = $(cfg.rangeSel).is(':checked');
const showP = $(cfg.phasesSel).is(':checked');
const showE = $(cfg.eventsSel).is(':checked');
const showV = $(cfg.vpSel).is(':checked');
if (!showR && !showP && !showE && !showV) return;
// WYCKOFF-MULTI-CYCLE-001:遍历 cycles;无则退化为顶层单段
const cycles = (w.cycles && w.cycles.length)
? w.cycles
: (w.trading_range ? [{
id: 0, status: 'ACTIVE', role: 'latest',
trading_range: w.trading_range, phases: w.phases, events: w.events,
volume_profile: w.volume_profile, volume_confirm: w.volume_confirm,
period: { start_time: w.trading_range.start_time, end_time: w.trading_range.end_time, bars: w.trading_range.bars }
}] : []);
const tfLabel = (cfg.tfLabel || cfg.tag || 'TF').toString().toUpperCase();
cycles.forEach(function(cycle) {
const tr = cycle.trading_range;
if (!tr) return;
const cid = (cycle.id != null) ? cycle.id : 0;
const isActive = String(cycle.status || '').toUpperCase() === 'ACTIVE';
const cTag = tfLabel + ' C' + cid + ' ';
try {
let t0 = toSec(tr.start_time || (cycle.period && cycle.period.start_time));
let t1 = toSec(tr.end_time || (cycle.period && cycle.period.end_time));
// 仅 ACTIVE 可拉到最新 K;历史用 period.end
if (isActive && !isNaN(lastKlineTime)) {
t1 = lastKlineTime;
} else if (cycle.period && cycle.period.end_time) {
t1 = toSec(cycle.period.end_time);
}
const hi = parseFloat(tr.high), lo = parseFloat(tr.low);
if (showR && !isNaN(hi) && !isNaN(lo)) {
drawBox(t0, t1, hi, lo, cfg.color);
if (isActive) {
mkPL(hi, cfg.color, cfg.hiTag, 0);
mkPL(lo, cfg.color, cfg.loTag, 0);
mkPL(tr.mid, cfg.color, cfg.midTag, 2);
}
}
if (showP && cycle.phases && cycle.phases.length && !isNaN(hi)) {
cycle.phases.forEach(function(ph) {
let p0 = toSec(ph.start_time);
let p1 = ph.end_time ? toSec(ph.end_time) : t1;
const sp = ensureSpan(p0, p1);
p0 = sp[0]; p1 = sp[1];
if (isNaN(p0) || isNaN(p1)) return;
const col = phaseColors[ph.phase] || '#95a5a6';
mainChart.addLineSeries({
color: col, lineWidth: 3, lastValueVisible: false, priceLineVisible: false
}).setData([{ time: p0, value: hi }, { time: p1, value: hi }]);
pushMarker({
time: p0, position: 'aboveBar', color: col, shape: 'square',
text: cTag + 'Phase ' + String(ph.phase || ''), size: 1
});
});
}
if (showV && isActive && cycle.volume_profile) {
const vp = cycle.volume_profile;
mkPL(vp.poc, cfg.vpColor, cfg.pocTag, 0);
mkPL(vp.vah, cfg.vpColor, cfg.vahTag, 2);
mkPL(vp.val, cfg.vpColor, cfg.valTag, 2);
const bins = (vp.bins || []).filter(function(b) { return b && b.volume > 0; })
.slice().sort(function(a, b) { return b.volume - a.volume; }).slice(0, 8);
let maxVol = 0;
bins.forEach(function(b) { if (b.volume > maxVol) maxVol = b.volume; });
const span = (!isNaN(t0) && !isNaN(t1) && t1 > t0) ? (t1 - t0) : barStep * 12;
bins.forEach(function(b) {
if (!b.volume || maxVol <= 0 || isNaN(t1)) return;
const wSec = Math.max(barStep, Math.floor(span * 0.12 * (b.volume / maxVol)));
let leftT = Math.max(isNaN(t0) ? (t1 - wSec) : t0, t1 - wSec);
if (leftT >= t1) leftT = t1 - barStep;
if (leftT >= t1) return;
const alpha = 0.25 + 0.55 * (b.volume / maxVol);
mainChart.addLineSeries({
color: cfg.vpRgb.replace('ALPHA', alpha.toFixed(2)),
lineWidth: 2, lastValueVisible: false, priceLineVisible: false
}).setData([{ time: leftT, value: b.price }, { time: t1, value: b.price }]);
});
}
if (showE && cycle.events && cycle.events.length) {
const checks = (cycle.volume_confirm && cycle.volume_confirm.event_checks) || {};
cycle.events.forEach(function(ev) {
const t = toSec(ev.time);
if (isNaN(t)) return;
const typ = ev.type || '';
const chk = checks[typ] || {};
const volOk = (chk.volume_ok != null) ? chk.volume_ok : ev.volume_ok;
const ok = volOk === true ? '✓' : (volOk === false ? '✗' : '');
pushMarker({
time: t,
position: (typ === 'Spring' || typ === 'LPS' || typ === 'SOW') ? 'belowBar' : 'aboveBar',
color: eventColors[typ] || '#7f8c8d',
shape: (typ === 'Spring' || typ === 'SOW' || typ === 'LPS') ? 'arrowDown' : 'arrowUp',
text: cTag + typ + ok,
size: 1
});
});
}
} catch (e) { console.error('区间叠层出错', cfg.name, 'C' + cid, e); }
});
};
if ($('#showMainWrRange').is(':checked') || $('#showMainWrPhases').is(':checked')
|| $('#showMainWrEvents').is(':checked') || $('#showMainWrVP').is(':checked')) {
drawOne(currentData.wyckoff, {
name: '主', tag: '', tfLabel: (currentData.timeframe || '4H'), color: '#3498db', vpColor: '#8e44ad',
vpRgb: 'rgba(142, 68, 173, ALPHA)',
rangeSel: '#showMainWrRange', phasesSel: '#showMainWrPhases',
eventsSel: '#showMainWrEvents', vpSel: '#showMainWrVP',
hiTag: 'WR.H', loTag: 'WR.L', midTag: 'WR.M',
pocTag: 'POC', vahTag: 'VAH', valTag: 'VAL'
});
}
if ($('#showElementWrRange').is(':checked') || $('#showElementWrPhases').is(':checked')
|| $('#showElementWrEvents').is(':checked') || $('#showElementWrVP').is(':checked')) {
drawOne(currentData.element_wyckoff, {
name: '次', tag: 'e', tfLabel: (currentData.element_timeframe || '2H'), color: '#e67e22', vpColor: '#d35400',
vpRgb: 'rgba(211, 84, 0, ALPHA)',
rangeSel: '#showElementWrRange', phasesSel: '#showElementWrPhases',
eventsSel: '#showElementWrEvents', vpSel: '#showElementWrVP',
hiTag: 'eWR.H', loTag: 'eWR.L', midTag: 'eWR.M',
pocTag: 'ePOC', vahTag: 'eVAH', valTag: 'eVAL'
});
}
if ($('#showSubSubWrRange').is(':checked') || $('#showSubSubWrPhases').is(':checked')
|| $('#showSubSubWrEvents').is(':checked') || $('#showSubSubWrVP').is(':checked')) {
drawOne(currentData.sub_sub_wyckoff, {
name: '次次', tag: 's', tfLabel: (currentData.sub_sub_timeframe || '1H'), color: '#27ae60', vpColor: '#16a085',
vpRgb: 'rgba(22, 160, 133, ALPHA)',
rangeSel: '#showSubSubWrRange', phasesSel: '#showSubSubWrPhases',
eventsSel: '#showSubSubWrEvents', vpSel: '#showSubSubWrVP',
hiTag: 'sWR.H', loTag: 'sWR.L', midTag: 'sWR.M',
pocTag: 'sPOC', vahTag: 'sVAH', valTag: 'sVAL'
});
}
// 同 BSP:写入 window,稍后与分型/买卖点一并 setMarkers
wrMarkers.sort(function(a, b) { return a.time - b.time; });
window.wrMarkers = wrMarkers;
if (typeof renderWyckoffCycleSummary === 'function') {
renderWyckoffCycleSummary();
}
})();
// 显示未完成中枢 - 分别处理主周期、次周期和次次周期
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 },
};
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 = [];
}
// 添加买卖点标记(旧版,保留兼容)
// 这里为了与主面板上的「买卖点」开关保持一致,
// 同时响应顶部的 `#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 = [];
}
// 绘制小周期分型标记(含次次周期)
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);
const trendRaw = (t.trend || '').toString().toUpperCase();
let timeAligned = seriesTimes.has(ts) ? ts : nearestTime(ts);
let marker = { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'square', size: 0.8 };
if (trendRaw === 'UP') {
marker = { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 };
} else if (trendRaw === 'DOWN') {
marker = { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 };
} else if (trendRaw === 'FLAT') {
marker = { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 };
} else {
// UNKNOWN 或其他
marker = { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 };
}
return marker;
});
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 trendRaw = (t.trend || '').toString().toUpperCase();
const timeAligned = candlesTimes.has(ts) ? ts : nearestTime(ts);
if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 };
if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 };
if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 };
return { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 };
});
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 subSubColor = '#00897b';
const subSubMarkers = currentData.sub_sub_klc_trend.map(t => {
const ts = Math.floor(new Date(t.time).getTime() / 1000);
const trendRaw = (t.trend || '').toString().toUpperCase();
const timeAligned = candlesTimesSs.has(ts) ? ts : nearestTimeSs(ts);
if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: subSubColor, shape: 'arrowUp', size: 0.4 };
if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: subSubColor, shape: 'arrowDown', size: 0.4 };
if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: subSubColor, shape: 'circle', size: 0.5 };
return { time: timeAligned, position: 'inBar', color: subSubColor, shape: 'square', size: 0.5 };
});
trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers);
}
// 合并标记并设置
const combinedMarkers = [
...(window.mainFxMarkers || []),
...allElementFxMarkers,
...(window.kluDivMarkersMain || []),
...(window.kluDivMarkersElement || []),
...(window.kluDivMarkersSubSub || []),
...trendMarkersToUse,
...(window.bspMarkers || []),
...(window.wrMarkers || [])
];
if (combinedMarkers.length > 0) {
console.log(
'合并设置', combinedMarkers.length, '个标记(主周期分型:',
(window.mainFxMarkers || []).length,
'个,小周期分型:', allElementFxMarkers.length,
'个,UnitTF:', (window.unittfMarkers || []).length,
'个,BSP标记:', (window.bspMarkers || []).length,
'个,区间标记:', (window.wrMarkers || []).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 trendRaw = (t.trend || '').toString().toUpperCase();
const timeAligned = seriesTimes.has(ts) ? ts : nearestTime(ts);
if (trendRaw === 'UP') {
return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 };
} else if (trendRaw === 'DOWN') {
return { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 };
} else if (trendRaw === 'FLAT') {
return { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 };
} else {
return { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 };
}
});
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 trendRaw = (t.trend || '').toString().toUpperCase();
const timeAligned = candlesTimes.has(ts) ? ts : nearestTime(ts);
if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 };
if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 };
if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 };
return { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 };
});
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 subSubColor2 = '#00897b';
const subSubMarkers2 = currentData.sub_sub_klc_trend.map(t => {
const ts = Math.floor(new Date(t.time).getTime() / 1000);
const trendRaw = (t.trend || '').toString().toUpperCase();
const timeAligned = candlesTimesSs2.has(ts) ? ts : nearestTimeSs2(ts);
if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: subSubColor2, shape: 'arrowUp', size: 0.4 };
if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: subSubColor2, shape: 'arrowDown', size: 0.4 };
if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: subSubColor2, shape: 'circle', size: 0.5 };
return { time: timeAligned, position: 'inBar', color: subSubColor2, shape: 'square', size: 0.5 };
});
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.wrMarkers || [])
];
if (onlyMainAndU.length > 0) {
console.log('仅设置', onlyMainAndU.length, '个主周期/UnitTF标记(主周期分型:', (window.mainFxMarkers || []).length, 'UnitTF:', (window.unittfMarkers || []).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);
}
}