Files
Chan/web/static/js/app/overlays.js
T
jackyu66gitandCursor 74dec4e50b refactor: 缠论引擎包化与 Web 分层(ECR-001)
将根目录引擎迁入 chanlun/ 并保留兼容 shim;拆分 TF_DF 与 web 服务;
前端模块化;strategies 改用 chanlun 导入;补充 ESS 文档与 golden 回归。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 18:48:20 +08:00

1246 lines
46 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.
/* overlays.js */
function clearEMA52Series() {
// 清理所有EMA52相关系列(包括线系列和标记系列)
if (tvWidget && tvWidget.series && tvWidget.series.ema52Series) {
tvWidget.series.ema52Series.forEach(series => {
try {
if (tvWidget.mainChart) {
tvWidget.mainChart.removeSeries(series);
}
} catch (e) {
console.warn('移除EMA52系列失败:', e);
}
});
tvWidget.series.ema52Series = [];
}
console.log('✅ EMA52系列已清理');
}
// 存储上次的EMA52数据,用于比较
let lastEMA52Data = null;
// 更新EMA52显示
function updateEMA52Display(data) {
console.log('更新EMA52显示', data.ema52_dict);
// 检查是否有EMA52数据
if (!data.ema52_dict || Object.keys(data.ema52_dict).length === 0) {
// 即使没有数据也要清理之前的系列
clearEMA52Series();
lastEMA52Data = null;
return;
}
// 检查数据是否与上次相同,如果相同则跳过更新
const currentDataStr = JSON.stringify(data.ema52_dict);
if (lastEMA52Data === currentDataStr) {
console.log('EMA52数据未变化,跳过更新');
return;
}
// 清除之前的EMA52线系列
clearEMA52Series();
// 保存当前数据
lastEMA52Data = currentDataStr;
// 定义时间周期的显示顺序和颜色
const timeframeColors = {
'1m': '#FF0000', // 红色
'3m': '#FF6600', // 橙红色
'5m': '#FF9900', // 橙色
'10m': '#DDAA00', // 黄色
'15m': '#FFCC00', // 黄色
'30m': '#99FF00', // 黄绿色
'1h': '#00FF00', // 绿色
'2h': '#00FF99', // 青绿色
'4h': '#00FFFF', // 青色
'6h': '#0099FF', // 蓝青色
'8h': '#0066FF', // 蓝色
'12h': '#3300FF', // 蓝紫色
'16h': '#6600FF', // 紫色
'1d': '#9900FF', // 紫红色
'2d': '#CC00FF', // 品红色
'3d': '#FF00CC', // 粉红色
};
// 按照预定义顺序排列时间周期
const orderedTimeframes = ['1m', '3m', '5m', '10m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '16h', '1d', '2d', '3d'];
// 获取主图表容器
const chartContainer = document.getElementById('tradingview_chart');
if (!chartContainer || !tvWidget.mainChart) {
return;
}
orderedTimeframes.forEach(timeframe => {
if (data.ema52_dict[timeframe] !== undefined && data.ema52_dict[timeframe] !== null) {
const value = data.ema52_dict[timeframe];
const color = timeframeColors[timeframe] || '#800080';
// 添加虚线到主图表
if (tvWidget.mainChart) {
try {
// 使用LightweightCharts的addLineSeries创建虚线
const lineSeries = tvWidget.mainChart.addLineSeries({
color: color,
lineWidth: 1,
lineStyle: 2, // 虚线样式
title: ``,
lastValueVisible: false, // 不在价格标尺显示数值
priceLineVisible: false, // 不显示默认价格线
crosshairMarkerVisible: false,
priceFormat: {
type: 'price',
precision: 2,
minMove: 0.01,
},
priceScaleId: 'right',
});
// 创建横线数据(使用K线数据的时间范围)
if (data.kline_data && data.kline_data.length > 0) {
const firstKline = data.kline_data[0];
const lastKline = data.kline_data[data.kline_data.length - 1];
const startTime = Math.floor(new Date(firstKline.date).getTime() / 1000);
const endTime = Math.floor(new Date(lastKline.date).getTime() / 1000);
const lineData = [
{ time: startTime, value: value },
{ time: endTime, value: value }
];
lineSeries.setData(lineData);
// 使用标记在K线右侧显示文字
if (data.kline_data && data.kline_data.length > 0) {
const lastKline = data.kline_data[data.kline_data.length - 1];
const lastTime = Math.floor(new Date(lastKline.date).getTime() / 1000);
// 计算时间间隔(用于右偏移)
let timeInterval = 60; // 默认1分钟
if (data.kline_data.length > 1) {
const secondLastKline = data.kline_data[data.kline_data.length - 2];
const secondLastTime = Math.floor(new Date(secondLastKline.date).getTime() / 1000);
timeInterval = lastTime - secondLastTime;
}
// 创建右偏移的时间点(在最后一根K线右边)
const rightOffsetTime = lastTime + timeInterval*100;
// 创建一个新的线系列用于显示文字标记
const markerSeries = tvWidget.mainChart.addLineSeries({
color: 'transparent',
lineWidth: 0,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
priceScaleId: 'right',
});
// 添加一个透明的数据点在右偏移位置
markerSeries.setData([{
time: rightOffsetTime,
value: value
}]);
// 在右偏移位置添加文字标记
markerSeries.setMarkers([{
time: rightOffsetTime,
position: 'inBar',
color: color,
shape: 'square',
text: ` ${timeframe} ${value.toFixed(2)}`,
size: 1,
}]);
// 保存标记系列引用
if (!tvWidget.series.ema52Series) {
tvWidget.series.ema52Series = [];
}
tvWidget.series.ema52Series.push(markerSeries);
}
}
// 保存系列引用以便后续清理
if (!tvWidget.series.ema52Series) {
tvWidget.series.ema52Series = [];
}
tvWidget.series.ema52Series.push(lineSeries);
} catch (e) {
console.warn('添加EMA52虚线失败:', timeframe, e);
}
}
}
});
}
// 获取随机颜色
function getRandomColor() {
const colors = ['#2962FF', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4',
'#FECA57', '#48CAE4', '#F38BA8', '#A8DADC', '#F1C0E8'];
return colors[Math.floor(Math.random() * colors.length)];
}
// 显示布林带配置弹窗
function showBBConfig() {
console.log('📂 显示布林带配置弹窗,设置为添加模式');
$('#bbConfigModal').css('display', 'flex');
// 重置表单
$('#bbType').val('Bollinger Bands');
$('#bbLength').val(20);
$('#bbStdDev').val(2);
$('#bbSource').val('close');
$('#bbLineWidth').val(2);
$('#bbLineStyle').val(0);
$('#bbColor').val(getRandomColor());
// 移除所有现有的点击事件,避免事件冲突
$('#bbConfigSubmitBtn').off('click');
// 设置按钮为添加模式
$('#bbConfigSubmitBtn').text('添加').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('🖱️ 点击了添加按钮(来自showBBConfig),准备添加新布林带');
addBollingerBand();
});
console.log('🔄 按钮已设置为添加模式');
// 更新预览
setTimeout(updateBBLinePreview, 50);
}
// 显示均线配置弹窗
function showMAConfig() {
console.log('📂 显示均线配置弹窗,设置为添加模式');
$('#maConfigModal').css('display', 'flex');
// 重置表单
$('#maType').val('SMA');
$('#maLength').val(20);
$('#maSource').val('close');
$('#maSmoothType').val('none');
$('#maSmoothLength').val(3).prop('disabled', true);
$('#maLineWidth').val(2);
$('#maLineStyle').val(0);
$('#maColor').val(getRandomColor());
// 移除所有现有的点击事件,避免事件冲突
$('#maConfigSubmitBtn').off('click');
// 设置按钮为添加模式
$('#maConfigSubmitBtn').text('添加').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('🖱️ 点击了添加按钮(来自showMAConfig),准备添加新均线');
addMovingAverage();
});
console.log('🔄 按钮已设置为添加模式');
// 更新预览
setTimeout(updateLinePreview, 50);
}
// 隐藏均线配置弹窗
function hideMAConfig() {
console.log('🔒 关闭均线配置弹窗');
$('#maConfigModal').css('display', 'none');
// 移除所有现有的点击事件
$('#maConfigSubmitBtn').off('click');
// 恢复按钮为添加模式 - 但不立即绑定事件,防止意外触发
$('#maConfigSubmitBtn').text('添加');
console.log('🔄 按钮已重置为添加模式,等待用户主动点击"添加均线"按钮');
}
// 点击弹窗外部关闭(MA
$(document).on('click', '#maConfigModal', function(e) {
if (e.target === this) {
hideMAConfig();
}
});
// 添加均线
function addMovingAverage() {
console.log('🚨🚨🚨 警告:addMovingAverage函数被调用了!!!');
console.log('📊 添加前均线配置数量:', movingAverages.length);
console.log('🔍 调用堆栈:', new Error().stack);
const config = {
id: ++maIdCounter,
type: $('#maType').val(),
length: parseInt($('#maLength').val()),
source: $('#maSource').val(),
smoothType: $('#maSmoothType').val(),
smoothLength: parseInt($('#maSmoothLength').val()),
lineWidth: parseInt($('#maLineWidth').val()),
lineStyle: parseInt($('#maLineStyle').val()),
color: $('#maColor').val(),
visible: true
};
console.log('📝 新均线配置:', {
id: config.id,
type: config.type,
length: config.length,
source: config.source,
color: config.color
});
// 验证输入
if (config.length < 1) {
alert('均线长度必须大于0');
return;
}
if (config.smoothType !== 'none' && (config.smoothLength < 1 || config.smoothLength > 50)) {
alert('平滑长度必须在1-50之间');
return;
}
movingAverages.push(config);
console.log('📊 添加后均线配置数量:', movingAverages.length);
hideMAConfig();
// 统一刷新(带重试,确保图表就绪)
refreshIndicatorsNowOrLater(5);
}
// 计算均线数据
function calculateMA() {
if (window.App && window.App.Indicators && typeof window.App.Indicators.calculateMA === 'function') {
return window.App.Indicators.calculateMA.apply(null, arguments);
}
console.warn('calculateMA 未就绪,返回空数组');
return [];
}
// 应用平滑处理
function applySmoothToMA(maData, smoothType, smoothLength) {
if (smoothType === 'none' || maData.length < smoothLength) {
return maData;
}
const result = [];
for (let i = smoothLength - 1; i < maData.length; i++) {
let value;
switch(smoothType) {
case 'SMA':
value = maData.slice(i - smoothLength + 1, i + 1)
.reduce((sum, item) => sum + item.value, 0) / smoothLength;
break;
case 'EMA':
const multiplier = 2 / (smoothLength + 1);
if (result.length === 0) {
// 第一个EMA值使用SMA作为种子值
value = maData.slice(i - smoothLength + 1, i + 1)
.reduce((sum, item) => sum + item.value, 0) / smoothLength;
} else {
// 后续EMA值使用标准公式
value = maData[i].value * multiplier + result[result.length - 1].value * (1 - multiplier);
}
break;
case 'WMA':
let weightSum = 0;
let valueSum = 0;
for (let j = 0; j < smoothLength; j++) {
const weight = j + 1;
weightSum += weight;
valueSum += maData[i - smoothLength + 1 + j].value * weight;
}
value = valueSum / weightSum;
break;
default:
value = maData[i].value;
}
result.push({
time: maData[i].time,
value: value
});
}
return result;
}
// 更新技术指标面板(统一面板)
function updateIndicatorPanel() {
const panel = $('#indicatorPanel');
if (movingAverages.length === 0 && bollingerBands.length === 0) {
panel.hide();
return;
}
let html = '';
let hasVisibleIndicator = false;
// 添加均线指标
movingAverages.forEach(ma => {
// 获取最新价格
const latestValue = ma.data && ma.data.length > 0 ?
ma.data[ma.data.length - 1].value.toFixed(2) : '--';
const smoothText = ma.smoothType !== 'none' ?
`, ${ma.smoothType}(${ma.smoothLength})` : '';
// 获取线条样式描述
const lineStyleNames = ['实线', '点线', '虚线', '大虚线'];
const styleText = ma.lineWidth && ma.lineStyle !== undefined ?
` ${ma.lineWidth}px ${lineStyleNames[ma.lineStyle] || '实线'}` : '';
// 显示所有均线(包括隐藏的),但用不同样式区分
const itemStyle = ma.visible ? '' : 'opacity: 0.5;';
const eyeIcon = ma.visible ? 'bi-eye' : 'bi-eye-slash';
html += `
<div class="indicator-item" data-indicator-type="ma" data-indicator-id="${ma.id}" style="${itemStyle}">
<div class="indicator-info">
<div class="indicator-color-indicator" style="background-color: ${ma.color}"></div>
<span class="indicator-label">${ma.type}(${ma.length})${smoothText}${styleText}</span>
<span class="indicator-value">${ma.visible ? latestValue : '--'}</span>
</div>
<div class="indicator-actions">
<button class="indicator-action-btn" onclick="toggleMAVisibility('${ma.id}')" title="${ma.visible ? '隐藏' : '显示'}">
<i class="bi ${eyeIcon}"></i>
</button>
<button class="indicator-action-btn" onclick="configMA('${ma.id}')" title="配置">
<i class="bi bi-gear"></i>
</button>
<button class="indicator-action-btn" onclick="deleteMA('${ma.id}')" title="删除">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
`;
if (ma.visible) {
hasVisibleIndicator = true;
}
});
// 添加布林带指标
bollingerBands.forEach(bb => {
// 获取最新价格
const latestValue = bb.data && bb.data.length > 0 ?
bb.data[bb.data.length - 1].middle.toFixed(2) : '--';
// 获取线条样式描述
const lineStyleNames = ['实线', '点线', '虚线', '大虚线'];
const styleText = bb.lineWidth && bb.lineStyle !== undefined ?
` ${bb.lineWidth}px ${lineStyleNames[bb.lineStyle] || '实线'}` : '';
// 显示所有布林带(包括隐藏的),但用不同样式区分
const itemStyle = bb.visible ? '' : 'opacity: 0.5;';
const eyeIcon = bb.visible ? 'bi-eye' : 'bi-eye-slash';
// 创建渐变颜色指示器显示三种颜色
const colorIndicatorStyle = `
background: linear-gradient(90deg,
${bb.upperColor || '#ff6b6b'} 0%,
${bb.middleColor || '#667eea'} 50%,
${bb.lowerColor || '#4ecdc4'} 100%);
border-radius: 2px;
`;
html += `
<div class="indicator-item" data-indicator-type="bb" data-indicator-id="${bb.id}" style="${itemStyle}">
<div class="indicator-info">
<div class="indicator-color-indicator" style="${colorIndicatorStyle}"></div>
<span class="indicator-label">BB(${bb.length}, ${bb.upperMultiplier || 2}, ${bb.lowerMultiplier || 2})${styleText}</span>
<span class="indicator-value">${bb.visible ? latestValue : '--'}</span>
</div>
<div class="indicator-actions">
<button class="indicator-action-btn" onclick="toggleBBVisibility('${bb.id}')" title="${bb.visible ? '隐藏' : '显示'}">
<i class="bi ${eyeIcon}"></i>
</button>
<button class="indicator-action-btn" onclick="configBB('${bb.id}')" title="配置">
<i class="bi bi-gear"></i>
</button>
<button class="indicator-action-btn" onclick="deleteBB('${bb.id}')" title="删除">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
`;
if (bb.visible) {
hasVisibleIndicator = true;
}
});
panel.html(html);
if (hasVisibleIndicator || movingAverages.length > 0 || bollingerBands.length > 0) {
panel.show();
} else {
panel.hide();
}
}
// 更新均线面板(兼容旧代码)
function updateMAPanel() {
updateIndicatorPanel();
}
// 切换均线可见性
function toggleMAVisibility(maId) {
console.log('👁️ 切换均线可见性,ID:', maId, 'Type:', typeof maId);
const ma = movingAverages.find(m => m.id == maId); // 使用==而不是===来兼容类型转换
if (ma) {
console.log('📝 找到均线,当前可见性:', ma.visible);
ma.visible = !ma.visible;
console.log('✅ 切换后可见性:', ma.visible);
// 直接使用前端数据重新计算均线
if (tvWidget.mainChart && currentData && currentData.kline_data) {
const candles = getCurrentCandleData();
addMovingAveragesToChart(candles);
}
// 刷新面板图标与数值
try { updateIndicatorPanel(); } catch(e) {}
} else {
console.error('❌ 未找到要切换可见性的均线,ID:', maId);
}
}
// 配置均线
function configMA(maId) {
console.log('⚙️ 开始配置均线,ID:', maId, 'Type:', typeof maId);
console.log('📊 当前所有均线ID:', movingAverages.map(m => ({id: m.id, type: typeof m.id})));
const ma = movingAverages.find(m => m.id == maId); // 使用==而不是===来兼容类型转换
if (!ma) {
console.error('❌ 未找到要配置的均线,ID:', maId);
return;
}
console.log('📝 找到要配置的均线:', ma.type, ma.length, ma.color);
// 填充表单
$('#maType').val(ma.type);
$('#maLength').val(ma.length);
$('#maSource').val(ma.source);
$('#maSmoothType').val(ma.smoothType);
$('#maSmoothLength').val(ma.smoothLength);
$('#maLineWidth').val(ma.lineWidth || 2);
$('#maLineStyle').val(ma.lineStyle || 0);
$('#maColor').val(ma.color);
// 启用/禁用平滑长度输入
$('#maSmoothLength').prop('disabled', ma.smoothType === 'none');
// 移除所有现有的点击事件,避免事件冲突
$('#maConfigSubmitBtn').off('click');
// 修改按钮为更新模式
$('#maConfigSubmitBtn').text('更新').on('click', (function(capturedMaId) {
return function(e) {
e.preventDefault();
e.stopPropagation();
console.log('🖱️ 点击了更新按钮,准备更新ID:', capturedMaId);
console.log('🔍 捕获的ID类型:', typeof capturedMaId);
updateMovingAverage(capturedMaId);
};
})(maId));
console.log('🔄 按钮已切换为更新模式');
$('#maConfigModal').css('display', 'flex');
// 更新预览
setTimeout(updateLinePreview, 50);
}
// 更新均线
function updateMovingAverage(maId) {
console.log('🔄 开始更新均线,ID:', maId, 'Type:', typeof maId);
console.log('📊 更新前均线配置数量:', movingAverages.length);
console.log('📊 当前所有均线:', movingAverages.map(m => ({id: m.id, type: typeof m.id, maType: m.type, length: m.length, color: m.color})));
const ma = movingAverages.find(m => m.id == maId); // 使用==而不是===来兼容类型转换
if (!ma) {
console.error('❌ 未找到要更新的均线配置,ID:', maId);
console.error('❌ 可用的均线ID:', movingAverages.map(m => m.id));
alert('错误:未找到要更新的均线配置!');
return;
}
console.log('📝 找到要更新的均线:', ma.type, ma.length, ma.color);
// 验证输入
const newLength = parseInt($('#maLength').val());
const newSmoothLength = parseInt($('#maSmoothLength').val());
const newSmoothType = $('#maSmoothType').val();
if (newLength < 1) {
alert('均线长度必须大于0');
return;
}
if (newSmoothType !== 'none' && (newSmoothLength < 1 || newSmoothLength > 50)) {
alert('平滑长度必须在1-50之间');
return;
}
// 记录更新前的配置
console.log('🔧 更新前配置:', {
type: ma.type,
length: ma.length,
source: ma.source,
color: ma.color
});
// 更新配置
ma.type = $('#maType').val();
ma.length = newLength;
ma.source = $('#maSource').val();
ma.smoothType = newSmoothType;
ma.smoothLength = newSmoothLength;
ma.lineWidth = parseInt($('#maLineWidth').val());
ma.lineStyle = parseInt($('#maLineStyle').val());
ma.color = $('#maColor').val();
// 记录更新后的配置
console.log('✅ 更新后配置:', {
type: ma.type,
length: ma.length,
source: ma.source,
color: ma.color
});
console.log('📊 更新后均线配置数量:', movingAverages.length);
hideMAConfig();
// 直接使用前端数据重新计算均线
if (tvWidget.mainChart && currentData && currentData.kline_data) {
const candles = getCurrentCandleData();
addMovingAveragesToChart(candles);
}
// 刷新面板显示
try { updateIndicatorPanel(); } catch(e) {}
}
// 删除均线
function deleteMA(maId) {
console.log('🗑️ 删除均线,ID:', maId, 'Type:', typeof maId);
console.log('📊 删除前均线配置数量:', movingAverages.length);
const beforeCount = movingAverages.length;
movingAverages = movingAverages.filter(m => m.id != maId); // 使用!=而不是!==来兼容类型转换
console.log('📊 删除后均线配置数量:', movingAverages.length);
console.log('✅ 是否成功删除:', beforeCount > movingAverages.length);
// 直接使用前端数据重新计算均线
if (tvWidget.mainChart && currentData && currentData.kline_data) {
const candles = getCurrentCandleData();
addMovingAveragesToChart(candles);
}
// 刷新技术指标面板并关闭可能残留的配置弹窗
try { updateIndicatorPanel(); } catch(e) {}
try { if ($('#maConfigModal').is(':visible')) { hideMAConfig(); } } catch(e) {}
}
// 获取当前K线数据的辅助函数(与基础显示的主/小/次次周期保持一致)
function getCurrentCandleData() {
if (!currentData || !currentData.kline_data) {
return [];
}
const useSubSubPeriod = $('#subSubPeriodKline').is(':checked') &&
currentData.sub_sub_kline_data &&
Array.isArray(currentData.sub_sub_kline_data);
const useElementPeriod = !useSubSubPeriod &&
$('#elementPeriodKline').is(':checked') &&
currentData.element_kline_data &&
Array.isArray(currentData.element_kline_data);
let source = currentData.kline_data;
if (useSubSubPeriod) {
source = currentData.sub_sub_kline_data;
} else if (useElementPeriod) {
source = currentData.element_kline_data;
}
const candles = source.map((kline) => {
const date = new Date(kline.date);
const timestamp = date.getTime() / 1000;
return {
time: timestamp,
open: parseFloat(kline.open),
high: parseFloat(kline.high),
low: parseFloat(kline.low),
close: parseFloat(kline.close),
};
});
return candles;
}
// 从蜡烛数据生成 Heikin-Ashi(平均K
function buildHeikinFromCandles(candles) {
if (!Array.isArray(candles) || candles.length === 0) return [];
const result = [];
let prevHaClose = (candles[0].open + candles[0].high + candles[0].low + candles[0].close) / 4;
let prevHaOpen = (candles[0].open + candles[0].close) / 2;
const firstHaHigh = Math.max(candles[0].high, prevHaOpen, prevHaClose);
const firstHaLow = Math.min(candles[0].low, prevHaOpen, prevHaClose);
result.push({ time: candles[0].time, open: prevHaOpen, high: firstHaHigh, low: firstHaLow, close: prevHaClose });
for (let i = 1; i < candles.length; i++) {
const c = candles[i];
const haClose = (c.open + c.high + c.low + c.close) / 4;
const haOpen = (prevHaOpen + prevHaClose) / 2;
const haHigh = Math.max(c.high, haOpen, haClose);
const haLow = Math.min(c.low, haOpen, haClose);
result.push({ time: c.time, open: haOpen, high: haHigh, low: haLow, close: haClose });
prevHaOpen = haOpen;
prevHaClose = haClose;
}
return result;
}
// 从分析数据生成KLC蜡烛数据
function buildKLCFromAnalysis(data) {
if (!data) return [];
// 根据基础显示的K线周期选择:次次周期 / 小周期 / 主周期
const useSubSubPeriod = $('#subSubPeriodKline').is(':checked') &&
data.sub_sub_klc_list &&
Array.isArray(data.sub_sub_klc_list);
const useElementPeriod = !useSubSubPeriod &&
$('#elementPeriodKline').is(':checked') &&
data.element_klc_list &&
Array.isArray(data.element_klc_list);
const klcList = useSubSubPeriod
? data.sub_sub_klc_list
: (useElementPeriod ? data.element_klc_list : data.klc_list);
if (!klcList || !Array.isArray(klcList)) return [];
const klcCandles = [];
// 遍历KLC列表,转换为蜡烛数据格式
klcList.forEach(klc => {
if (!klc || !klc.date) return;
const date = new Date(klc.date);
const timestamp = date.getTime() / 1000;
const candle = {
time: timestamp,
open: klc.open || 0,
high: klc.high || 0,
low: klc.low || 0,
close: klc.close || 0
};
klcCandles.push(candle);
});
return klcCandles;
}
// 计算默认砖大小(优先使用ATR的最新非零值,否则按收盘价的0.5%)
function computeDefaultBrickSize(candles) {
try {
const useElementPeriod = $('#elementPeriodKline').is(':checked') && currentData.element_kline_data && Array.isArray(currentData.element_kline_data);
const atrArr = useElementPeriod ? (currentData.element_atr || currentData.atr) : currentData.atr;
if (Array.isArray(atrArr) && atrArr.length > 0) {
for (let i = atrArr.length - 1; i >= 0; i--) {
const v = parseFloat(atrArr[i]);
if (!isNaN(v) && v > 0) {
return v;
}
}
}
} catch(e) {}
const last = candles && candles.length ? candles[candles.length - 1].close : 0;
const size = last * 0.005; // 默认0.5%
return size > 0 ? size : 1;
}
// 从蜡烛数据生成Renko砖(返回伪K线数据用于CandlestickSeries显示)
function buildRenkoFromCandles(candles) {
if (!Array.isArray(candles) || candles.length === 0) return [];
const tryBuild = (size) => {
const bricks = [];
let lastClose = candles[0].close;
for (let i = 1; i < candles.length; i++) {
// 确保同一根K内多砖的时间唯一且递增
let nextTime = candles[i].time;
const price = candles[i].close;
let diff = price - lastClose;
// 向上生成砖
while (diff >= size) {
const open = lastClose;
const close = lastClose + size;
bricks.push({ time: nextTime, open: open, high: close, low: open, close: close });
lastClose = close;
diff = price - lastClose;
nextTime += 1; // 递增1秒,保证唯一
}
// 向下生成砖
while (diff <= -size) {
const open = lastClose;
const close = lastClose - size;
bricks.push({ time: nextTime, open: open, high: open, low: close, close: close });
lastClose = close;
diff = price - lastClose;
nextTime += 1; // 递增1秒,保证唯一
}
}
return bricks;
};
// 计算初始砖大小,若无砖则逐步缩小后重试,避免不显示
let brickSize = computeDefaultBrickSize(candles);
let result = tryBuild(brickSize);
let attempts = 0;
while (result.length === 0 && attempts < 4) {
brickSize *= 0.5; // 缩小一半后重试
result = tryBuild(brickSize);
attempts += 1;
}
return result;
}
// 指标统一刷新(若图表未就绪则自动重试)
function refreshIndicatorsNowOrLater(retries) {
try {
if (tvWidget && tvWidget.mainChart && currentData && (currentData.kline_data || currentData.element_kline_data)) {
const candles = getCurrentCandleData();
if (candles && candles.length > 0) {
addMovingAveragesToChart(candles);
addBollingerBandsToChart(candles);
try { updateIndicatorPanel(); } catch(e) {}
return;
}
}
} catch(e) {}
if (retries && retries > 0) {
setTimeout(function(){ refreshIndicatorsNowOrLater(retries - 1); }, 200);
}
}
// 添加均线到图表
function addMovingAveragesToChart() {
if (window.App && window.App.Charts && typeof window.App.Charts.addMovingAveragesToChart === 'function') {
return window.App.Charts.addMovingAveragesToChart.apply(null, arguments);
}
console.warn('addMovingAveragesToChart 未就绪');
}
// 监听平滑类型变化(保留,UI 逻辑)
$(document).on('change', '#maSmoothType', function() {
const isNone = $(this).val() === 'none';
$('#maSmoothLength').prop('disabled', isNone);
});
// 重复定义的 showBBConfig 已移除(合并到上方唯一实现)
// 隐藏布林带配置弹窗
function hideBBConfig() {
console.log('🔒 关闭布林带配置弹窗');
$('#bbConfigModal').css('display', 'none');
// 移除所有现有的点击事件
$('#bbConfigSubmitBtn').off('click');
// 恢复按钮为添加模式 - 但不立即绑定事件,防止意外触发
$('#bbConfigSubmitBtn').text('添加');
console.log('🔄 按钮已重置为添加模式,等待用户主动点击"添加布林带"按钮');
}
// 添加布林带
function addBollingerBand() {
console.log('🚨🚨🚨 警告:addBollingerBand函数被调用了!!!');
console.log('📊 添加前布林带配置数量:', bollingerBands.length);
console.log('🔍 调用堆栈:', new Error().stack);
const config = {
id: ++bbIdCounter,
type: $('#bbType').val(),
length: parseInt($('#bbLength').val()),
upperMultiplier: parseFloat($('#bbUpperMultiplier').val()),
lowerMultiplier: parseFloat($('#bbLowerMultiplier').val()),
source: $('#bbSource').val(),
lineWidth: parseInt($('#bbLineWidth').val()),
lineStyle: parseInt($('#bbLineStyle').val()),
upperColor: $('#bbUpperColor').val(),
middleColor: $('#bbMiddleColor').val(),
lowerColor: $('#bbLowerColor').val(),
visible: true
};
console.log('📝 新布林带配置:', {
id: config.id,
type: config.type,
length: config.length,
upperMultiplier: config.upperMultiplier,
lowerMultiplier: config.lowerMultiplier,
source: config.source,
colors: {
upper: config.upperColor,
middle: config.middleColor,
lower: config.lowerColor
}
});
// 验证输入
if (config.length < 1) {
alert('布林带长度必须大于0');
return;
}
if (config.upperMultiplier < 0.1) {
alert('上轨倍数必须大于0.1');
return;
}
if (config.lowerMultiplier < 0.1) {
alert('下轨倍数必须大于0.1');
return;
}
bollingerBands.push(config);
console.log('📊 添加后布林带配置数量:', bollingerBands.length);
hideBBConfig();
// 统一刷新(带重试,确保图表就绪)
refreshIndicatorsNowOrLater(5);
}
// 计算布林带数据
function calculateBB() {
if (window.App && window.App.Indicators && typeof window.App.Indicators.calculateBB === 'function') {
return window.App.Indicators.calculateBB.apply(null, arguments);
}
console.warn('calculateBB 未就绪,返回空数组');
return [];
}
// 更新布林带面板(兼容旧代码)
function updateBBPanel() {
updateIndicatorPanel();
}
// 切换布林带可见性
function toggleBBVisibility(bbId) {
console.log('👁️ 切换布林带可见性,ID:', bbId, 'Type:', typeof bbId);
const bb = bollingerBands.find(b => b.id == bbId); // 使用==而不是===来兼容类型转换
if (bb) {
console.log('📝 找到布林带,当前可见性:', bb.visible);
bb.visible = !bb.visible;
console.log('✅ 切换后可见性:', bb.visible);
// 直接使用前端数据重新计算布林带
if (tvWidget.mainChart && currentData && currentData.kline_data) {
const candles = getCurrentCandleData();
addBollingerBandsToChart(candles);
addMovingAveragesToChart(candles); // 同时更新均线
}
// 刷新面板图标与数值
try { updateIndicatorPanel(); } catch(e) {}
} else {
console.error('❌ 未找到要切换可见性的布林带,ID:', bbId);
}
}
// 配置布林带
function configBB(bbId) {
console.log('⚙️ 开始配置布林带,ID:', bbId, 'Type:', typeof bbId);
console.log('📊 当前所有布林带ID:', bollingerBands.map(b => ({id: b.id, type: typeof b.id})));
const bb = bollingerBands.find(b => b.id == bbId); // 使用==而不是===来兼容类型转换
if (!bb) {
console.error('❌ 未找到要配置的布林带,ID:', bbId);
return;
}
console.log('📝 找到要配置的布林带:', bb.type, bb.length, bb.upperMultiplier, bb.lowerMultiplier, bb.upperColor, bb.middleColor, bb.lowerColor);
// 填充表单
$('#bbType').val(bb.type);
$('#bbLength').val(bb.length);
$('#bbUpperMultiplier').val(bb.upperMultiplier || 2);
$('#bbLowerMultiplier').val(bb.lowerMultiplier || 2);
$('#bbSource').val(bb.source);
$('#bbLineWidth').val(bb.lineWidth || 2);
$('#bbLineStyle').val(bb.lineStyle || 0);
$('#bbUpperColor').val(bb.upperColor || '#ff6b6b');
$('#bbMiddleColor').val(bb.middleColor || '#667eea');
$('#bbLowerColor').val(bb.lowerColor || '#4ecdc4');
// 移除所有现有的点击事件,避免事件冲突
$('#bbConfigSubmitBtn').off('click');
// 修改按钮为更新模式
$('#bbConfigSubmitBtn').text('更新').on('click', (function(capturedBbId) {
return function(e) {
e.preventDefault();
e.stopPropagation();
console.log('🖱️ 点击了更新按钮,准备更新ID:', capturedBbId);
console.log('🔍 捕获的ID类型:', typeof capturedBbId);
updateBollingerBand(capturedBbId);
};
})(bbId));
console.log('🔄 按钮已切换为更新模式');
$('#bbConfigModal').css('display', 'flex');
// 更新预览
setTimeout(updateBBLinePreview, 50);
}
// 更新布林带
function updateBollingerBand(bbId) {
console.log('🔄 开始更新布林带,ID:', bbId, 'Type:', typeof bbId);
console.log('📊 更新前布林带配置数量:', bollingerBands.length);
console.log('📊 当前所有布林带:', bollingerBands.map(b => ({id: b.id, type: typeof b.id, bbType: b.type, length: b.length, upperMultiplier: b.upperMultiplier, lowerMultiplier: b.lowerMultiplier, upperColor: b.upperColor, middleColor: b.middleColor, lowerColor: b.lowerColor})));
const bb = bollingerBands.find(b => b.id == bbId); // 使用==而不是===来兼容类型转换
if (!bb) {
console.error('❌ 未找到要更新的布林带配置,ID:', bbId);
console.error('❌ 可用的布林带ID:', bollingerBands.map(b => b.id));
alert('错误:未找到要更新的布林带配置!');
return;
}
console.log('📝 找到要更新的布林带:', bb.type, bb.length, bb.upperMultiplier, bb.lowerMultiplier, bb.upperColor, bb.middleColor, bb.lowerColor);
// 验证输入
const newLength = parseInt($('#bbLength').val());
const newUpperMultiplier = parseFloat($('#bbUpperMultiplier').val());
const newLowerMultiplier = parseFloat($('#bbLowerMultiplier').val());
if (newLength < 1) {
alert('布林带长度必须大于0');
return;
}
if (newUpperMultiplier < 0.1) {
alert('上轨倍数必须大于0.1');
return;
}
if (newLowerMultiplier < 0.1) {
alert('下轨倍数必须大于0.1');
return;
}
// 记录更新前的配置
console.log('🔧 更新前配置:', {
type: bb.type,
length: bb.length,
upperMultiplier: bb.upperMultiplier,
lowerMultiplier: bb.lowerMultiplier,
source: bb.source,
colors: {
upper: bb.upperColor,
middle: bb.middleColor,
lower: bb.lowerColor
}
});
// 更新配置
bb.type = $('#bbType').val();
bb.length = newLength;
bb.upperMultiplier = newUpperMultiplier;
bb.lowerMultiplier = newLowerMultiplier;
bb.source = $('#bbSource').val();
bb.lineWidth = parseInt($('#bbLineWidth').val());
bb.lineStyle = parseInt($('#bbLineStyle').val());
bb.upperColor = $('#bbUpperColor').val();
bb.middleColor = $('#bbMiddleColor').val();
bb.lowerColor = $('#bbLowerColor').val();
// 记录更新后的配置
console.log('✅ 更新后配置:', {
type: bb.type,
length: bb.length,
upperMultiplier: bb.upperMultiplier,
lowerMultiplier: bb.lowerMultiplier,
source: bb.source,
colors: {
upper: bb.upperColor,
middle: bb.middleColor,
lower: bb.lowerColor
}
});
console.log('📊 更新后布林带配置数量:', bollingerBands.length);
hideBBConfig();
// 直接使用前端数据重新计算布林带
if (tvWidget.mainChart && currentData && currentData.kline_data) {
const candles = getCurrentCandleData();
addBollingerBandsToChart(candles);
}
// 刷新面板显示
try { updateIndicatorPanel(); } catch(e) {}
// 刷新面板显示
try { updateIndicatorPanel(); } catch(e) {}
}
// 删除布林带
function deleteBB(bbId) {
console.log('🗑️ 删除布林带,ID:', bbId, 'Type:', typeof bbId);
console.log('📊 删除前布林带配置数量:', bollingerBands.length);
const beforeCount = bollingerBands.length;
bollingerBands = bollingerBands.filter(b => b.id != bbId); // 使用!=而不是!==来兼容类型转换
console.log('📊 删除后布林带配置数量:', bollingerBands.length);
console.log('✅ 是否成功删除:', beforeCount > bollingerBands.length);
// 直接使用前端数据重新计算布林带
if (tvWidget.mainChart && currentData && currentData.kline_data) {
const candles = getCurrentCandleData();
addBollingerBandsToChart(candles);
}
// 刷新技术指标面板并关闭可能残留的配置弹窗
try { updateIndicatorPanel(); } catch(e) {}
try { if ($('#bbConfigModal').is(':visible')) { hideBBConfig(); } } catch(e) {}
}
// 添加布林带到图表
function addBollingerBandsToChart() {
if (window.App && window.App.Charts && typeof window.App.Charts.addBollingerBandsToChart === 'function') {
return window.App.Charts.addBollingerBandsToChart.apply(null, arguments);
}
console.warn('addBollingerBandsToChart 未就绪');
}
// 更新均线预览
function updateLinePreview() {
// 检查是否在均线配置窗口
if ($('#maConfigModal').is(':visible')) {
const color = $('#maColor').val();
const width = $('#maLineWidth').val();
const style = $('#maLineStyle').val();
const line = $('#previewLine');
line.attr('stroke', color);
line.attr('stroke-width', width);
// 设置线条样式
switch(parseInt(style)) {
case 0: // 实线
line.attr('stroke-dasharray', 'none');
break;
case 1: // 点线
line.attr('stroke-dasharray', '2,3');
break;
case 2: // 虚线
line.attr('stroke-dasharray', '5,5');
break;
case 3: // 大虚线
line.attr('stroke-dasharray', '10,5');
break;
}
}
}
// 更新布林带预览
function updateBBLinePreview() {
const upperColor = $('#bbUpperColor').val();
const middleColor = $('#bbMiddleColor').val();
const lowerColor = $('#bbLowerColor').val();
const width = $('#bbLineWidth').val();
const style = $('#bbLineStyle').val();
const upperLine = $('#bbPreviewUpper');
const middleLine = $('#bbPreviewMiddle');
const lowerLine = $('#bbPreviewLower');
// 设置各条线的颜色
upperLine.attr('stroke', upperColor);
middleLine.attr('stroke', middleColor);
lowerLine.attr('stroke', lowerColor);
// 设置线条宽度和样式
[upperLine, middleLine, lowerLine].forEach(line => {
line.attr('stroke-width', width);
// 设置线条样式
switch(parseInt(style)) {
case 0: // 实线
line.attr('stroke-dasharray', 'none');
break;
case 1: // 点线
line.attr('stroke-dasharray', '2,3');
break;
case 2: // 虚线
line.attr('stroke-dasharray', '5,5');
break;
case 3: // 大虚线
line.attr('stroke-dasharray', '10,5');
break;
}
});
}
// 监听配置变化以更新预览
$(document).on('change', '#maColor, #maLineWidth, #maLineStyle', updateLinePreview);
$(document).on('change', '#bbUpperColor, #bbMiddleColor, #bbLowerColor, #bbLineWidth, #bbLineStyle', updateBBLinePreview);
// 点击弹窗外部关闭
$(document).on('click', '#bbConfigModal', function(e) {
if (e.target === this) {
hideBBConfig();
}
});
// 初始化技术指标下拉菜单
function initIndicatorDropdown() {
// 确保Bootstrap下拉菜单正常工作
try {
// 如果Bootstrap没有正确加载,添加手动下拉菜单功能
$('#indicatorDropdown').off('click').on('click', function(e) {
e.preventDefault();
const $menu = $(this).next('.dropdown-menu');
// 切换菜单显示状态
if ($menu.hasClass('show')) {
$menu.removeClass('show');
} else {
// 隐藏其他所有下拉菜单
$('.dropdown-menu').removeClass('show');
$menu.addClass('show');
}
});
// 点击菜单外部关闭下拉菜单
$(document).on('click', function(e) {
if (!$(e.target).closest('.indicator-dropdown').length) {
$('.dropdown-menu').removeClass('show');
}
});
console.log('✅ 技术指标下拉菜单初始化完成');
} catch (error) {
console.error('❌ 技术指标下拉菜单初始化失败:', error);
}
}
// 页面加载完成后初始化