This commit is contained in:
jackyu66git
2025-06-18 00:56:41 +08:00
parent 3dc7dd5322
commit de756e38eb
3 changed files with 338 additions and 53 deletions
+12 -1
View File
@@ -52,12 +52,18 @@ exchange = ccxt.binance({
# 时间周期映射
TIMEFRAMES = {
'1m': '1分钟',
'3m': '3分钟',
'5m': '5分钟',
'15m': '15分钟',
'30m': '30分钟',
'1h': '1小时',
'2h': '2小时',
'4h': '4小时',
'6h': '6小时',
'8h': '8小时',
'12h': '12小时',
'1d': '日线',
'3d': '3日',
'1w': '周线',
'1M': '月线',
}
@@ -115,8 +121,10 @@ def get_crypto_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=
batch_size = 1000 # 默认批次大小
if timeframe in ['1m', '3m', '5m']:
batch_size = 500 # 分钟级数据减少批次大小
elif timeframe in ['15m', '30m', '1h']:
elif timeframe in ['15m', '30m', '1h', '2h']:
batch_size = 1000
elif timeframe in ['4h', '6h', '8h', '12h']:
batch_size = 1200 # 小时级数据可以获取更多
else:
batch_size = 1500 # 日线及以上可以获取更多
@@ -280,6 +288,9 @@ def add_indicators(df):
df['ma250'] = (ta.MA(df, timeperiod=250)).fillna(0)
df['rsi'] = ta.RSI(df, timeperiod=14)
# 计算ATR (平均真实波幅)
df['atr'] = ta.ATR(df, timeperiod=14).fillna(0)
# 计算布林带 (当前周期 - 20周期,2标准差)
bb = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0)
df['bb_upper'] = bb['upperband'].fillna(0)
+16 -6
View File
@@ -186,11 +186,18 @@ class ChinaStockData:
"""将时间周期转换为akshare的period参数"""
mapping = {
'1m': '1', # 1分钟
'3m': '5', # 3分钟 (A股不支持3分钟,使用5分钟替代)
'5m': '5', # 5分钟
'15m': '15', # 15分钟
'30m': '30', # 30分钟
'1h': '60', # 60分钟
'2h': '60', # 2小时 (A股不支持2小时,使用1小时替代)
'4h': '60', # 4小时 (A股不支持4小时,使用1小时替代)
'6h': '60', # 6小时 (A股不支持6小时,使用1小时替代)
'8h': '60', # 8小时 (A股不支持8小时,使用1小时替代)
'12h': '60', # 12小时 (A股不支持12小时,使用1小时替代)
'1d': 'daily', # 日线
'3d': 'daily', # 3日 (A股不支持3日,使用日线替代)
'1w': 'weekly',# 周线
'1M': 'monthly'# 月线
}
@@ -235,7 +242,7 @@ class ChinaStockData:
# 分钟级数据,每次获取7天
batch_days = 7
elif period == '60':
# 小时级数据,每次获取30天
# 小时级数据(所有小时级别都映射到60分钟),每次获取30天
batch_days = 30
else:
# 日线及以上,每次获取365天
@@ -587,7 +594,7 @@ class ChinaStockData:
df['date'] = df['date'].dt.normalize() + pd.Timedelta(hours=15)
# 对于分钟级数据,过滤非交易时间的数据
elif timeframe in ['1m', '5m', '15m', '30m', '1h']:
elif timeframe in ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h']:
# 过滤交易日
df = df[df['date'].apply(self.is_trading_day)]
@@ -651,7 +658,7 @@ class ChinaStockData:
return df
# 对于分钟级数据,创建完整的交易时间序列
if timeframe in ['1m', '5m', '15m', '30m', '1h']:
if timeframe in ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h']:
# 获取数据的开始和结束时间
start_date = df['date'].min().date()
end_date = df['date'].max().date()
@@ -660,8 +667,11 @@ class ChinaStockData:
complete_times = []
current_date = start_date
# 获取时间间隔(分钟)
freq_map = {'1m': 1, '5m': 5, '15m': 15, '30m': 30, '1h': 60}
# 获取时间间隔(分钟)- 注意A股新增的时间周期都映射到了已有的时间间隔
freq_map = {
'1m': 1, '3m': 5, '5m': 5, '15m': 15, '30m': 30,
'1h': 60, '2h': 60, '4h': 60, '6h': 60, '8h': 60, '12h': 60
}
freq_minutes = freq_map.get(timeframe, 5)
while current_date <= end_date:
@@ -769,7 +779,7 @@ class ChinaStockData:
df[col] = df[col].replace([np.nan, np.inf, -np.inf], 0)
# 确保时间序列连续性(仅对分钟级数据)
if timeframe in ['1m', '5m', '15m', '30m', '1h']:
if timeframe in ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h']:
df = self.fill_trading_gaps(df, timeframe)
# 最后再次检查并清理任何剩余的NaN值
+310 -46
View File
@@ -322,9 +322,21 @@
<div class="col-md-1">
<label for="timeframe" class="form-label">时间周期:</label>
<select id="timeframe" class="form-select">
{% for value, label in timeframes.items() %}
<option value="{{ value }}" {% if value == '5m' %}selected{% endif %}>{{ label }}</option>
{% endfor %}
<option value="1m">1分钟</option>
<option value="3m">3分钟</option>
<option value="5m" selected>5分钟</option>
<option value="15m">15分钟</option>
<option value="30m">30分钟</option>
<option value="1h">1小时</option>
<option value="2h">2小时</option>
<option value="4h">4小时</option>
<option value="6h">6小时</option>
<option value="8h">8小时</option>
<option value="12h">12小时</option>
<option value="1d">1日</option>
<option value="3d">3日</option>
<option value="1w">1周</option>
<option value="1M">1月</option>
</select>
</div>
<div class="col-md-1">
@@ -440,9 +452,21 @@
<div class="d-flex align-items-center mb-2">
<label for="elementTimeframe" class="form-label me-2 mb-0">次级别时间周期:</label>
<select id="elementTimeframe" class="form-select form-select-sm me-2" style="width: 120px;">
{% for value, label in timeframes.items() %}
<option value="{{ value }}" {% if value == '1m' %}selected{% endif %}>{{ label }}</option>
{% endfor %}
<option value="1m" selected>1分钟</option>
<option value="3m">3分钟</option>
<option value="5m">5分钟</option>
<option value="15m">15分钟</option>
<option value="30m">30分钟</option>
<option value="1h">1小时</option>
<option value="2h">2小时</option>
<option value="4h">4小时</option>
<option value="6h">6小时</option>
<option value="8h">8小时</option>
<option value="12h">12小时</option>
<option value="1d">1日</option>
<option value="3d">3日</option>
<option value="1w">1周</option>
<option value="1M">1月</option>
</select>
</div>
@@ -663,12 +687,19 @@
<div class="col-md-3">
<label for="filterTimeframe" class="form-label">时间周期:</label>
<select id="filterTimeframe" class="form-select">
<option value="1m">1分钟</option>
<option value="3m">3分钟</option>
<option value="5m">5分钟</option>
<option value="15m">15分钟</option>
<option value="30m">30分钟</option>
<option value="1h">1小时</option>
<option value="2h">2小时</option>
<option value="4h">4小时</option>
<option value="6h">6小时</option>
<option value="8h">8小时</option>
<option value="12h">12小时</option>
<option value="1d" selected>1日</option>
<option value="3d">3日</option>
<option value="1w">1周</option>
<option value="1M">1月</option>
</select>
@@ -736,6 +767,7 @@
macdLineSeries: null,
signalLineSeries: null,
histogramSeries: null,
atrLineSeries: null,
mainBiSeries: [],
mainSegSeries: [],
mainZsSeries: [],
@@ -1396,22 +1428,34 @@
volumeChartContainer.style.right = '0';
volumeChartContainer.style.borderTop = '1px solid #e0e0e0';
// 如果需要显示MACD,创建MACD容器
// 如果需要显示MACD,创建MACD和ATR容器
let macdChartContainer = null;
let atrChartContainer = null;
if (showMacd) {
// 设置各图表高度 - 为个图表分配合理比例,主图表适度增加高度
mainChartContainer.style.height = '55%'; // 主图占55%(约385px
volumeChartContainer.style.top = '55%';
volumeChartContainer.style.height = '22.5%'; // 成交量图占22.5%(约157.5px
// 设置各图表高度 - 为个图表分配合理比例
mainChartContainer.style.height = '45%'; // 主图占45%(约315px
volumeChartContainer.style.top = '45%';
volumeChartContainer.style.height = '18.5%'; // 成交量图占18.5%(约129.5px
// 创建MACD容器
macdChartContainer = document.createElement('div');
macdChartContainer.style.width = '100%';
macdChartContainer.style.height = '22.5%'; // MACD图占22.5%(约157.5px
macdChartContainer.style.height = '18.5%'; // MACD图占18.5%(约129.5px
macdChartContainer.style.position = 'absolute';
macdChartContainer.style.top = '77.5%'; // 从77.5%位置开始
macdChartContainer.style.top = '63.5%'; // 从63.5%位置开始
macdChartContainer.style.left = '0';
macdChartContainer.style.right = '0';
macdChartContainer.style.borderTop = '1px solid #e0e0e0';
// 创建ATR容器
atrChartContainer = document.createElement('div');
atrChartContainer.style.width = '100%';
atrChartContainer.style.height = '18%'; // ATR图占18%(约126px
atrChartContainer.style.position = 'absolute';
atrChartContainer.style.top = '82%'; // 从82%位置开始
atrChartContainer.style.left = '0';
atrChartContainer.style.right = '0';
atrChartContainer.style.borderTop = '1px solid #e0e0e0';
} else {
// 不显示MACD时的高度 - 主图和成交量图分配
mainChartContainer.style.height = '72%'; // 主图占72%(约504px
@@ -1421,7 +1465,10 @@
container.appendChild(mainChartContainer);
container.appendChild(volumeChartContainer);
if (showMacd) container.appendChild(macdChartContainer);
if (showMacd) {
container.appendChild(macdChartContainer);
container.appendChild(atrChartContainer);
}
// 防止同步过程中的无限循环
let syncInProgress = false;
@@ -1436,6 +1483,8 @@
chartHeight = volumeChartContainer.clientHeight;
} else if (chartType === 'macd') {
chartHeight = macdChartContainer ? macdChartContainer.clientHeight : 0;
} else if (chartType === 'atr') {
chartHeight = atrChartContainer ? atrChartContainer.clientHeight : 0;
} else {
chartHeight = mainChartContainer.clientHeight;
}
@@ -1585,8 +1634,29 @@
// 创建MACD图表(如果需要)
let macdChart = null;
let atrChart = null;
if (showMacd) {
macdChart = LightweightCharts.createChart(macdChartContainer, createChartOptions(false, 'macd'));
macdChart = LightweightCharts.createChart(macdChartContainer, createChartOptions(false, 'macd'));
atrChart = LightweightCharts.createChart(atrChartContainer, createChartOptions(false, 'atr'));
// 强制所有图表使用相同的时间刻度配置,确保时间对齐
const timeScaleOptions = {
borderVisible: false,
timeVisible: true,
secondsVisible: false,
tickMarkMaxCharacterLength: 8,
};
// 应用统一的时间刻度到所有图表
mainChart.timeScale().applyOptions(timeScaleOptions);
volumeChart.timeScale().applyOptions(timeScaleOptions);
macdChart.timeScale().applyOptions(timeScaleOptions);
atrChart.timeScale().applyOptions(timeScaleOptions);
mainChart.timeScale().applyOptions(timeScaleOptions);
volumeChart.timeScale().applyOptions(timeScaleOptions);
macdChart.timeScale().applyOptions(timeScaleOptions);
atrChart.timeScale().applyOptions(timeScaleOptions);
}
// 创建蜡烛图系列并设置数据
@@ -1623,9 +1693,10 @@
// 转换成交量数据 - 始终使用主K线周期数据
let volumes = [];
// 使用与K线和MACD相同的数据源选择逻辑
const volumeDataSource = useElementPeriod ? currentData.element_kline_data : currentData.kline_data;
// 强制使用主周期数据源,确保与MACD、ATR图表时间完全一致
const volumeDataSource = currentData.kline_data; // 总是使用主周期数据
console.log('成交量数据源选择:', useElementPeriod ? '次周期' : '主周期');
console.log('成交量数据源: 主周期(强制与MACD、ATR一致)');
console.log('成交量数据长度:', volumeDataSource.length);
if (volumeDataSource && Array.isArray(volumeDataSource)) {
@@ -1688,13 +1759,11 @@
const signalData = [];
const histogramData = [];
// 使用与K线数据相同的数据源确保时间对齐
const klineDataSource = useElementPeriod ? currentData.element_kline_data : currentData.kline_data;
const macdDataSource = useElementPeriod ?
(currentData.element_macd || currentData.macd) : // 如果有次周期MACD数据则使用,否则使用主周期
currentData.macd; // 主周期使用主周期MACD数据
// 强制使用主周期数据源确保与ATR图表时间完全一致
const klineDataSource = currentData.kline_data; // 总是使用主周期数据
const macdDataSource = currentData.macd; // 总是使用主周期MACD数据
console.log('MACD数据源选择:', useElementPeriod ? '次周期' : '主周期');
console.log('MACD数据源: 主周期(强制与ATR一致)');
console.log('K线数据长度:', klineDataSource.length);
console.log('MACD数据:', macdDataSource);
@@ -1735,6 +1804,71 @@
tvWidget.series.histogramSeries = histogramSeries;
}
// 添加ATR图表 - 独立显示ATR数据
if (showMacd && currentData.kline_data && Array.isArray(currentData.kline_data)) {
// 创建ATR线
const atrLineSeries = atrChart.addLineSeries({
color: '#FF9800',
lineWidth: 2,
title: 'ATR',
lastValueVisible: false,
priceLineVisible: false,
baseLineVisible: false,
crosshairMarkerVisible: false,
pointMarkersVisible: false,
});
// 提取ATR数据
const atrData = [];
// 强制使用主周期数据源,确保与MACD图表时间完全一致
const klineDataSource = currentData.kline_data; // 总是使用主周期数据
console.log('ATR数据源: 主周期(强制与MACD一致)');
console.log('ATR K线数据长度:', klineDataSource.length);
// 创建一个隐藏的基础数据系列来保持时间轴对齐
const baseLineSeries = atrChart.addLineSeries({
color: 'transparent',
lineWidth: 0,
lastValueVisible: false,
priceLineVisible: false,
baseLineVisible: false,
crosshairMarkerVisible: false,
pointMarkersVisible: false,
visible: false
});
// 添加完整的时间数据(透明)来保持时间轴连续
const baseData = [];
for (let i = 0; i < klineDataSource.length; i++) {
const kline = klineDataSource[i];
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
baseData.push({
time: timestamp,
value: 0.1 // 使用很小的值,不会显示
});
// 只添加ATR>0的有效数据到可见系列
if (kline.atr && kline.atr > 0) {
atrData.push({
time: timestamp,
value: kline.atr
});
}
}
// 先设置基础数据保持时间轴
baseLineSeries.setData(baseData);
// 再设置ATR可见数据
atrLineSeries.setData(atrData);
console.log('ATR基础数据点数:', baseData.length);
console.log('ATR可见数据点数:', atrData.length);
tvWidget.series.atrLineSeries = atrLineSeries;
}
// 实现三图联动滚动
// 同步图表的时间范围
@@ -1748,7 +1882,8 @@
syncInProgress = true;
console.log('🚀 开始同步图表,来源:',
sourceChart === mainChart ? '主图' :
sourceChart === volumeChart ? '成交量图' : 'MACD图');
sourceChart === volumeChart ? '成交量图' :
sourceChart === macdChart ? 'MACD图' : 'ATR图');
try {
if (sourceChart && sourceChart.timeScale) {
@@ -1787,6 +1922,16 @@
}
}
// 同步ATR图
if (showMacd && atrChart && sourceChart !== atrChart && atrChart.timeScale) {
try {
atrChart.timeScale().setVisibleLogicalRange(logicalRange);
console.log('✅ ATR图同步完成');
} catch (e) {
console.error('❌ ATR图同步失败:', e);
}
}
// 保存当前的可见范围到全局状态
if (tvWidget && tvWidget.state) {
tvWidget.state.logicalRange = logicalRange;
@@ -1812,7 +1957,8 @@
let localDragStates = {
main: false,
volume: false,
macd: false
macd: false,
atr: false
};
// 全局鼠标抬起事件(只添加一次)
@@ -1828,10 +1974,15 @@
// 为每个图表添加事件监听
const addChartSyncEvents = (chartContainer, chart) => {
console.log('为图表添加同步事件监听:', chart === mainChart ? '主图' : chart === volumeChart ? '成交量图' : 'MACD图');
console.log('为图表添加同步事件监听:',
chart === mainChart ? '主图' :
chart === volumeChart ? '成交量图' :
chart === macdChart ? 'MACD图' : 'ATR图');
// 确定当前图表类型
const chartType = chart === mainChart ? 'main' : chart === volumeChart ? 'volume' : 'macd';
const chartType = chart === mainChart ? 'main' :
chart === volumeChart ? 'volume' :
chart === macdChart ? 'macd' : 'atr';
// 使用LightweightCharts内置的时间范围变化事件(这是最可靠的方法)
chart.timeScale().subscribeVisibleTimeRangeChange(() => {
@@ -1890,6 +2041,9 @@
if (showMacd && macdChart) {
addChartSyncEvents(macdChartContainer, macdChart);
}
if (showMacd && atrChart) {
addChartSyncEvents(atrChartContainer, atrChart);
}
// 窗口大小变化时重绘图表
window.addEventListener('resize', () => {
@@ -1913,6 +2067,14 @@
});
}
// 调整ATR图大小
if (showMacd && atrChart && atrChartContainer) {
atrChart.applyOptions({
width: atrChartContainer.clientWidth,
height: atrChartContainer.clientHeight
});
}
// 重新同步 - 使用主图作为同步源
setTimeout(() => {
if (mainChart) {
@@ -3035,15 +3197,50 @@
if (showMacd && macdChart) {
macdChart.timeScale().fitContent();
}
if (showMacd && atrChart) {
atrChart.timeScale().fitContent();
}
// 保存图表对象
tvWidget.mainChart = mainChart;
tvWidget.volumeChart = volumeChart;
tvWidget.macdChart = macdChart;
tvWidget.atrChart = atrChart;
tvWidget.state.isInitialized = true;
// 绑定同步事件
bindSyncEvents(mainChartContainer, volumeChartContainer, macdChartContainer, mainChart, volumeChart, macdChart, showMacd);
bindSyncEvents(mainChartContainer, volumeChartContainer, macdChartContainer, atrChartContainer, mainChart, volumeChart, macdChart, atrChart, showMacd);
// 强制同步所有图表时间范围(确保时间对齐)
setTimeout(() => {
if (mainChart && mainChart.timeScale) {
// 先让所有图表自适应内容
mainChart.timeScale().fitContent();
if (volumeChart) volumeChart.timeScale().fitContent();
if (showMacd && macdChart) macdChart.timeScale().fitContent();
if (showMacd && atrChart) atrChart.timeScale().fitContent();
// 等待适应完成后强制同步时间范围
setTimeout(() => {
const mainRange = mainChart.timeScale().getVisibleLogicalRange();
console.log('主图时间范围:', mainRange);
if (mainRange && volumeChart && volumeChart.timeScale) {
volumeChart.timeScale().setVisibleLogicalRange(mainRange);
console.log('成交量图时间范围已同步');
}
if (mainRange && showMacd && macdChart && macdChart.timeScale) {
macdChart.timeScale().setVisibleLogicalRange(mainRange);
console.log('MACD图时间范围已同步');
}
if (mainRange && showMacd && atrChart && atrChart.timeScale) {
atrChart.timeScale().setVisibleLogicalRange(mainRange);
console.log('ATR图时间范围已同步');
}
console.log('所有图表时间范围已强制同步完成');
}, 50);
}
}, 200);
// 只有在时间输入框都为空时才设置图表默认时间范围
if (!$('#start_time').val() && !$('#end_time').val()) {
@@ -3051,7 +3248,7 @@
}
// 添加买卖点提示
setupTooltip(mainChart, [], [], mainChartContainer, volumeChartContainer, macdChartContainer, volumeChart, macdChart, showMacd);
setupTooltip(mainChart, [], [], mainChartContainer, volumeChartContainer, macdChartContainer, atrChartContainer, volumeChart, macdChart, atrChart, showMacd);
console.log('图表初始化完成');
} catch (e) {
@@ -3128,18 +3325,9 @@
tvWidget.series.lineSeries.setData(lineData);
}
// 更新成交量数据
// 更新成交量数据 - 强制使用主周期数据源确保时间对齐
let volumes = [];
if (useElementPeriod && currentData.element_kline_data && Array.isArray(currentData.element_kline_data)) {
volumes = currentData.element_kline_data.map(kline => {
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
return {
time: timestamp,
value: parseFloat(kline.volume),
color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)',
};
});
} else if (currentData.kline_data && Array.isArray(currentData.kline_data)) {
if (currentData.kline_data && Array.isArray(currentData.kline_data)) {
volumes = currentData.kline_data.map(kline => {
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
return {
@@ -3191,6 +3379,26 @@
tvWidget.series.histogramSeries.setData(histogramData);
}
// 更新ATR数据 - 保持时间轴对齐
if (currentData.kline_data && Array.isArray(currentData.kline_data) && tvWidget.series.atrLineSeries) {
const atrData = [];
for (let i = 0; i < currentData.kline_data.length; i++) {
const kline = currentData.kline_data[i];
const timestamp = Math.floor(new Date(kline.date).getTime() / 1000);
// 只添加ATR>0的有效数据,不显示ATR=0的点
if (kline.atr && kline.atr > 0) {
atrData.push({
time: timestamp,
value: kline.atr
});
}
}
tvWidget.series.atrLineSeries.setData(atrData);
}
// 先恢复可视范围,避免先显示默认范围再跳转的跳跃效果
if (tvWidget.mainChart && (tvWidget.state.logicalRange || tvWidget.state.visibleRange)) {
console.log('回放模式:优先恢复视图范围以避免跳跃');
@@ -3206,10 +3414,12 @@
tvWidget.mainChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
if (tvWidget.atrChart) tvWidget.atrChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
} else if (tvWidget.state.visibleRange) {
tvWidget.mainChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
if (tvWidget.atrChart) tvWidget.atrChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
}
}, 20);
}
@@ -3231,7 +3441,7 @@
}
}
function bindSyncEvents(mainChartContainer, volumeChartContainer, macdChartContainer, mainChart, volumeChart, macdChart, showMacd) {
function bindSyncEvents(mainChartContainer, volumeChartContainer, macdChartContainer, atrChartContainer, mainChart, volumeChart, macdChart, atrChart, showMacd) {
// 防止同步过程中的无限循环
let syncInProgress = false;
@@ -3239,7 +3449,8 @@
let localDragStates = {
main: false,
volume: false,
macd: false
macd: false,
atr: false
};
// 同步图表的时间范围
@@ -3253,7 +3464,8 @@
syncInProgress = true;
console.log('🚀 开始同步图表,来源:',
sourceChart === mainChart ? '主图' :
sourceChart === volumeChart ? '成交量图' : 'MACD图');
sourceChart === volumeChart ? '成交量图' :
sourceChart === macdChart ? 'MACD图' : 'ATR图');
try {
if (sourceChart && sourceChart.timeScale) {
@@ -3292,6 +3504,16 @@
}
}
// 同步ATR图
if (showMacd && atrChart && sourceChart !== atrChart && atrChart.timeScale) {
try {
atrChart.timeScale().setVisibleLogicalRange(logicalRange);
console.log('✅ ATR图同步完成');
} catch (e) {
console.error('❌ ATR图同步失败:', e);
}
}
// 保存当前的可见范围到全局状态
if (tvWidget && tvWidget.state) {
tvWidget.state.logicalRange = logicalRange;
@@ -3315,10 +3537,15 @@
// 为每个图表添加事件监听
const addChartSyncEvents = (chartContainer, chart) => {
console.log('为图表添加同步事件监听:', chart === mainChart ? '主图' : chart === volumeChart ? '成交量图' : 'MACD图');
console.log('为图表添加同步事件监听:',
chart === mainChart ? '主图' :
chart === volumeChart ? '成交量图' :
chart === macdChart ? 'MACD图' : 'ATR图');
// 确定当前图表类型
const chartType = chart === mainChart ? 'main' : chart === volumeChart ? 'volume' : 'macd';
const chartType = chart === mainChart ? 'main' :
chart === volumeChart ? 'volume' :
chart === macdChart ? 'macd' : 'atr';
// 使用LightweightCharts内置的时间范围变化事件(这是最可靠的方法)
chart.timeScale().subscribeVisibleTimeRangeChange(() => {
@@ -3381,6 +3608,9 @@
if (showMacd && macdChartContainer && macdChart) {
addChartSyncEvents(macdChartContainer, macdChart);
}
if (showMacd && atrChartContainer && atrChart) {
addChartSyncEvents(atrChartContainer, atrChart);
}
// 窗口大小变化时重绘图表
window.addEventListener('resize', () => {
@@ -3408,6 +3638,14 @@
});
}
// 调整ATR图大小
if (showMacd && atrChart && atrChartContainer) {
atrChart.applyOptions({
width: atrChartContainer.clientWidth,
height: atrChartContainer.clientHeight
});
}
// 重新同步 - 使用主图作为同步源
setTimeout(() => {
if (mainChart) {
@@ -3417,7 +3655,7 @@
});
}
function setupTooltip(mainChart, buyMarkers = [], sellMarkers = [], mainChartContainer, volumeChartContainer, macdChartContainer, volumeChart, macdChart, showMacd) {
function setupTooltip(mainChart, buyMarkers = [], sellMarkers = [], mainChartContainer, volumeChartContainer, macdChartContainer, atrChartContainer, volumeChart, macdChart, atrChart, showMacd) {
// 调试变量
window.debugMode = true;
@@ -3451,6 +3689,8 @@
existingVolumeLines.forEach(line => line.remove());
const existingMacdLines = document.querySelectorAll('.macd-crosshair-line');
existingMacdLines.forEach(line => line.remove());
const existingAtrLines = document.querySelectorAll('.atr-crosshair-line');
existingAtrLines.forEach(line => line.remove());
// 获取时间对应的坐标位置
const mainTimeCoordinate = mainChart.timeScale().timeToCoordinate(param.time);
@@ -3495,6 +3735,26 @@
document.body.appendChild(macdLine);
}
}
// 如果有ATR图,也在ATR图上绘制垂直线
if (showMacd && 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);
}
}
}
} catch (e) {
console.debug('十字线同步出错:', e);
@@ -3506,6 +3766,8 @@
existingVolumeLines.forEach(line => line.remove());
const existingMacdLines = document.querySelectorAll('.macd-crosshair-line');
existingMacdLines.forEach(line => line.remove());
const existingAtrLines = document.querySelectorAll('.atr-crosshair-line');
existingAtrLines.forEach(line => line.remove());
} catch (e) {
console.debug('清除十字线时出错:', e);
}
@@ -4037,6 +4299,7 @@
tvWidget.mainChart = null;
tvWidget.volumeChart = null;
tvWidget.macdChart = null;
tvWidget.atrChart = null;
// 重置系列数据
tvWidget.series = {
candleSeries: null,
@@ -4045,6 +4308,7 @@
macdLineSeries: null,
signalLineSeries: null,
histogramSeries: null,
atrLineSeries: null,
mainBiSeries: [],
mainSegSeries: [],
mainZsSeries: [],