Add ATR
This commit is contained in:
+12
-1
@@ -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
@@ -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
@@ -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: [],
|
||||
|
||||
Reference in New Issue
Block a user