/* 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 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) 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([]); }; var onRange = function () { requestAnimationFrame(redraw); }; try { mainChart.timeScale().subscribeVisibleLogicalRangeChange(onRange); } catch (e) {} try { mainChart.timeScale().subscribeVisibleTimeRangeChange(onRange); } catch (e) {} var ro = null; if (typeof ResizeObserver !== 'undefined') { ro = new ResizeObserver(onRange); ro.observe(mainChartContainer); } window._fxBoxOverlayCleanup = function () { try { mainChart.timeScale().unsubscribeVisibleLogicalRangeChange(onRange); } catch (e) {} try { mainChart.timeScale().unsubscribeVisibleTimeRangeChange(onRange); } 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); requestAnimationFrame(redraw); setTimeout(redraw, 50); } 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: `${point.desc || (type > 0 ? '买点' : '卖点')}
时间: ${formatTime(point.time)}
价格: ${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 = `
时间: ${formattedTime}
` + (priceInfo ? `
${priceInfo}
` : ''); 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('

'); 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: `
主周期${fx.is_bottom ? '底分型' : '顶分型'}(合): ${fx.fx_type}
强度分数: ${fx.fx_strength}分
强度等级: ${fx.fx_strength_level}
是否强分型: ${fx.is_strong_fx ? '是' : '否'}
价格: ${price.toFixed(4)}
时间: ${fx.time}
` }; 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: `
主周期${fx.is_bottom ? '底分型' : '顶分型'}(原): ${fx.fx_type}
强度分数: ${fx.fx_strength}分
强度等级: ${fx.fx_strength_level}
是否强分型: ${fx.is_strong_fx ? '是' : '否'}
价格: ${price.toFixed(4)}
时间: ${fx.time}
` }; 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: `
小周期${fx.is_bottom ? '底分型' : '顶分型'}(合): ${fx.fx_type}
强度分数: ${fx.fx_strength}分
强度等级: ${fx.fx_strength_level}
是否强分型: ${fx.is_strong_fx ? '是' : '否'}
价格: ${price.toFixed(4)}
时间: ${fx.time}
` }; 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: `
小周期${fx.is_bottom ? '底分型' : '顶分型'}(原): ${fx.fx_type}
强度分数: ${fx.fx_strength}分
强度等级: ${fx.fx_strength_level}
是否强分型: ${fx.is_strong_fx ? '是' : '否'}
价格: ${price.toFixed(4)}
时间: ${fx.time}
` }; 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 }]); const leftSeries = mainChart.addLineSeries({ color: boxColor, lineWidth: 1, lineStyle: 2, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: false, }); safeOverlayLineSetData(leftSeries, [{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); const rightSeries = mainChart.addLineSeries({ color: boxColor, lineWidth: 1, lineStyle: 2, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: false, }); safeOverlayLineSetData(rightSeries, [{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); if (!tvWidget.series.subSubKlcFxBoxSeries) tvWidget.series.subSubKlcFxBoxSeries = []; tvWidget.series.subSubKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); } } } 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); } } } } }