Add replay good now

This commit is contained in:
jackyu66git
2025-06-10 18:01:42 +08:00
parent 05bab03b8c
commit 3dc7dd5322
3 changed files with 637 additions and 316 deletions
+255 -47
View File
@@ -130,45 +130,36 @@ def get_crypto_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=
request_count = 0
max_requests = 50 # 最大请求次数,防止无限循环
print(f"开始分批获取加密货币数据: {symbol}, {timeframe}")
# 分页加载数据
while request_count < max_requests:
request_count += 1
print(f"批次 {request_count}: 获取数据 since={current_since}, limit={batch_size}")
try:
# 获取当前页的数据
ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=current_since, limit=batch_size)
# 如果没有获取到数据,结束循环
if not ohlcv or len(ohlcv) == 0:
print(f"批次 {request_count}: 未获取到数据,结束")
break
# 将获取到的数据添加到总列表中
all_ohlcv.extend(ohlcv)
print(f"批次 {request_count}: 获取到 {len(ohlcv)} 条记录")
# 获取最后一条数据的时间戳
last_timestamp = ohlcv[-1][0]
# 如果已达到结束时间,结束循环
if until and last_timestamp >= until:
print(f"批次 {request_count}: 已达到结束时间,结束")
break
# 如果获取的数据条数小于限制数,说明已经获取完所有数据
if len(ohlcv) < batch_size:
print(f"批次 {request_count}: 数据不足批次大小,已获取完所有数据")
break
# 更新下一页的开始时间(加1毫秒避免重复)
current_since = last_timestamp + 1
except Exception as e:
print(f"批次 {request_count} 获取失败: {e}")
# 如果单个批次失败,继续尝试下一个批次
if current_since:
# 尝试增加时间跳过可能的问题时间点
@@ -181,7 +172,6 @@ def get_crypto_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=
# 数据为空的情况
if not all_ohlcv or len(all_ohlcv) == 0:
print(f"未获取到数据: {symbol}, {timeframe}")
return None
# 转换为DataFrame
@@ -201,21 +191,16 @@ def get_crypto_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=
# 限制数据条数的逻辑 - 优先考虑时间范围
if start_time and end_time:
# 如果指定了明确的时间范围,返回该时间范围内的所有数据
print(f"用户指定了时间范围,返回完整数据 {len(df)} 条记录")
if len(df) > 10000: # 防止数据量过大,设置一个合理的上限
print(f"警告:数据量过大({len(df)}条),为保证性能将限制为最新的10000条记录")
df = df.tail(10000).reset_index(drop=True)
elif limit and len(df) > limit:
# 如果没有指定明确时间范围,使用默认的limit限制
print(f"未指定明确时间范围,应用默认限制,返回最新的 {limit} 条记录")
df = df.tail(limit).reset_index(drop=True)
# 如果过滤后没有数据,返回None
if len(df) == 0:
print("过滤后无数据")
return None
print(f"成功获取加密货币数据: {len(df)} 条记录 (共 {request_count} 个批次)")
return df
except Exception as e:
@@ -265,17 +250,14 @@ def get_a_stock_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time
# 如果用户指定了时间范围,优先获取该范围内的所有数据
actual_limit = limit
if start_date and end_date:
print(f"用户指定了时间范围 {start_date}{end_date},将获取该范围内的所有数据")
actual_limit = None # 不限制数据条数,获取完整时间范围数据
# 调用A股数据获取器
df = china_stock.get_kl_data(symbol, timeframe, start_date, end_date, actual_limit)
if df is None:
print(f"未获取到A股数据: {symbol}")
return None
print(f"获取到A股数据: {len(df)} 条记录")
return df
except Exception as e:
@@ -385,21 +367,11 @@ def analyze_chan(df, timeframe='1d'):
"zs_algo": "normal", # 使用标准中枢算法提高稳定性 over_seg, normal, auto
"bi_algo": "normal",
})
print(f"[{timeframe}] 使用配置: {config.__dict__}")
# 检查数据量是否足够进行缠论分析
if len(df) < 30:
print(f"[{timeframe}] 警告:数据量过少({len(df)}条),可能影响买卖点识别准确性")
else:
print(f"[{timeframe}] 数据量充足:{len(df)}条K线数据")
# 将数据设置到WebDataAPI
from DataAPI.WebDataAPI import WebDataAPI
WebDataAPI.set_data("WEB_DATA", df)
print(f"[{timeframe}] 数据已设置到WebDataAPI,时间范围: {df.iloc[0]['date']} ~ {df.iloc[-1]['date']}")
# 创建CChan实例,使用自定义数据源
print(f"[{timeframe}] 创建CChan实例,级别列表: {[kl_type]}")
chan = CChan(
code="WEB_DATA",
begin_time=None,
@@ -410,12 +382,8 @@ def analyze_chan(df, timeframe='1d'):
autype=AUTYPE.QFQ,
)
# CChan会自动加载数据,无需手动触发
print(f"[{timeframe}] CChan实例创建完成")
# 获取分析结果
kline_list = chan[kl_type]
print(f"[{timeframe}] 获取到kline_list,类型: {type(kline_list)}")
# 提取笔列表
bi_list = []
@@ -435,10 +403,8 @@ def analyze_chan(df, timeframe='1d'):
# 直接从KLine_List获取买卖点
buy_sell_points = []
try:
print(f"[{timeframe}] 分析买卖点,kl_type={kl_type}")
if hasattr(kline_list, 'bs_point_lst') and kline_list.bs_point_lst:
bsp_list = sorted(kline_list.bs_point_lst.lst, key=lambda x: x.klu.time)
print(f"[{timeframe}] 从KLine_List获取到的买卖点数量: {len(bsp_list)}")
for i, bsp in enumerate(bsp_list):
# 根据买卖点类型选择正确的价格
if bsp.is_buy:
@@ -454,25 +420,14 @@ def analyze_chan(df, timeframe='1d'):
'price': price,
'desc': f"{bsp.type2str()}"
})
# 添加调试信息,打印前5个买卖点和最后2个买卖点
if i < 5 or i >= len(bsp_list) - 2:
time_str = str(bsp.klu.time) if hasattr(bsp.klu.time, '__str__') else 'N/A'
print(f"[{timeframe}] 买卖点{i+1}/{len(bsp_list)}: 类型={'买点' if bsp.is_buy else '卖点'}, 时间={time_str}, 价格={price}, KLU价格范围=[{bsp.klu.low}, {bsp.klu.high}], 描述={bsp.type2str()}")
else:
print(f"[{timeframe}] KLine_List没有买卖点列表或bs_point_lst为空")
print(f"[{timeframe}] kline_list属性: {[attr for attr in dir(kline_list) if not attr.startswith('_')]}")
buy_sell_points = []
except Exception as e:
print(f"[{timeframe}] 获取买卖点失败: {e}")
traceback.print_exc()
buy_sell_points = []
# 提取分型信息
klc_fx_info = []
klu_fx_info = []
bsp_list = chan.get_bsp()
for bsp in bsp_list:
print(bsp.klu.time, bsp.type2str(), bsp.is_buy)
# 从K线列表中提取分型信息
if hasattr(kline_list, 'lst'):
for klc in kline_list.lst:
@@ -502,8 +457,10 @@ def analyze_chan(df, timeframe='1d'):
'fx_confirmed': True
})
print(f"chan.py分析完成: 笔{len(bi_list)}个, 线段{len(seg_list)}个, 中枢{len(zs_list)}个, 买卖点{len(buy_sell_points)}")
print(f"kline_list类型: {type(kline_list)}, 属性: {dir(kline_list) if hasattr(kline_list, '__dict__') else 'N/A'}")
# 只在数据量较少时打印分析结果摘要,避免在回放时产生过多日志
if len(df) <= 50:
print(f"chan.py分析完成: 笔{len(bi_list)}个, 线段{len(seg_list)}个, 中枢{len(zs_list)}个, 买卖点{len(buy_sell_points)}")
return {
'klc_list': kline_list.lst if hasattr(kline_list, 'lst') else [],
@@ -1195,5 +1152,256 @@ def calculate_bi_macd_divergence(bi_list):
return bi_macd_divs
@app.route('/api/replay_data')
def get_replay_data():
"""生成回放数据接口 - 为每个时间点计算当时可用的数据"""
symbol = request.args.get('symbol', 'SOL/USDT:USDT')
timeframe = request.args.get('timeframe', '5m')
# 验证交易对不为空
if not symbol or symbol.strip() == '':
print(f"错误: 空交易对")
return jsonify({'error': '交易对不能为空'})
# 获取时间范围参数
start_time = request.args.get('start_time')
end_time = request.args.get('end_time')
# 获取客户端请求的时区
client_timezone = request.args.get('timezone', 'Asia/Shanghai')
# 获取分形元素时间周期
element_timeframe = request.args.get('element_timeframe')
print(f"回放数据API请求参数: symbol={symbol}, timeframe={timeframe}, element_timeframe={element_timeframe}")
print(f"时间范围: start_time={start_time}, end_time={end_time}")
# 验证小周期是否小于主周期
if element_timeframe and not is_smaller_or_equal_timeframe(element_timeframe, timeframe):
print(f"错误: 元素周期 {element_timeframe} 大于主周期 {timeframe}")
return jsonify({'error': '分形元素时间周期必须小于或等于主图表时间周期'})
# 获取完整数据
df = get_kl_data(symbol, timeframe, start_time=start_time, end_time=end_time)
if df is None:
print(f"错误: 获取数据失败 - symbol={symbol}, timeframe={timeframe}")
return jsonify({'error': '获取数据失败'})
if len(df) == 0:
print(f"错误: 所选时间范围内没有数据 - symbol={symbol}, timeframe={timeframe}")
return jsonify({'error': '所选时间范围内没有数据'})
# 使用客户端指定的时区
client_tz = timezone(client_timezone)
# 预先获取小周期数据(如果需要的话),避免在循环中重复拉取
element_df = None
if element_timeframe:
element_df = get_kl_data(symbol, element_timeframe, start_time=start_time, end_time=end_time)
# 生成回放数据
replay_data = {}
print(f"开始生成回放数据,主周期: {len(df)} 条记录")
# 为每个时间点计算当时可用的数据
for i in range(len(df)):
# 获取到当前时间点的数据子集
current_df = df.iloc[:i+1].copy()
# 添加技术指标(包括布林带)
current_df = add_indicators(current_df)
# 进行缠论分析
analysis_result = analyze_chan(current_df, timeframe)
# 计算MACD
macd_data = calculate_macd(current_df)
# 计算笔的MACD背离值
bi_macd_divs = calculate_bi_macd_divergence(analysis_result['bi_list'])
# 获取买卖点
all_trade_points = analysis_result['trade_points']
# 构建当前时间点的数据
current_data = {
'kline_data': clean_dataframe_for_json(current_df).to_dict('records'),
'bi_list': [{
'start_time': format_time_safely(bi.begin_klc.time_end, client_tz),
'end_time': format_time_safely(bi.end_klc.time_end, client_tz) if bi.end_klc else None,
'start_price': bi.begin_klc.low if convert_direction(bi.dir) == 1 else bi.begin_klc.high,
'end_price': bi.end_klc.high if convert_direction(bi.dir) == 1 else bi.end_klc.low if bi.end_klc else None,
'direction': convert_direction(bi.dir),
'macd_div': float(bi_macd_divs.get(idx, 0))
} for idx, bi in enumerate(analysis_result['bi_list']) if bi.end_klc],
'seg_list': [{
'start_time': format_time_safely(seg.start_bi.begin_klc.time_end, client_tz),
'end_time': format_time_safely(seg.end_bi.end_klc.time_end, client_tz) if seg.end_bi else None,
'start_price': seg.start_bi.begin_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.begin_klc.high,
'end_price': seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low if seg.end_bi else None,
'direction': convert_direction(seg.dir)
} for seg in analysis_result['seg_list'] if seg.end_bi],
'zs_list': [{
'start_time': format_time_safely(zs.begin.time, client_tz),
'end_time': format_time_safely(zs.end.time, client_tz) if zs.end else None,
'zg': zs.high,
'zd': zs.low,
'is_sure': zs.is_sure
} for zs in analysis_result['zs_list'] if zs.end],
'uncompleted_zs_list': [{
'start_time': format_time_safely(zs.begin.time, client_tz),
'end_time': None,
'zg': zs.high,
'zd': zs.low,
'is_sure': zs.is_sure
} for zs in analysis_result['zs_list'] if not zs.is_sure],
'trade_points': [{
'type': point['type'],
'time': format_time_safely(point['time'], client_tz),
'price': point['price'],
'desc': point['desc']
} for point in all_trade_points],
'macd': macd_data,
'bollinger': {
'upper': current_df['bb_upper'].tolist(),
'middle': current_df['bb_middle'].tolist(),
'lower': current_df['bb_lower'].tolist()
},
'element_bollinger': {
'upper': current_df['element_bb_upper'].tolist(),
'middle': current_df['element_bb_middle'].tolist(),
'lower': current_df['element_bb_lower'].tolist()
},
'klc_fx_info': [{
'time': format_time_safely(point['time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']),
'fx_strength_level': str(point['fx_strength_level']),
'is_strong_fx': bool(point['is_strong_fx'])
} for point in analysis_result['klc_fx_info']],
'klu_fx_info': [{
'time': format_time_safely(point['time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']),
'fx_strength_level': str(point['fx_strength_level']),
'is_strong_fx': bool(point['is_strong_fx']),
'fx_confirmed': bool(point['fx_confirmed'])
} for point in analysis_result['klu_fx_info']]
}
# 如果有小周期数据,也要计算小周期的历史数据
if element_timeframe and element_df is not None and len(element_df) > 0:
# 找到对应当前主周期时间点的小周期数据截止位置
current_time = current_df['date'].iloc[-1]
element_subset = element_df[element_df['date'] <= current_time]
if len(element_subset) > 0:
# 添加小周期技术指标
element_subset = add_indicators(element_subset)
# 对小周期数据进行缠论分析
element_analysis = analyze_chan(element_subset, element_timeframe)
# 计算小周期MACD数据
element_macd_data = calculate_macd(element_subset)
# 计算小周期笔的MACD背离值
element_bi_macd_divs = calculate_bi_macd_divergence(element_analysis['bi_list'])
# 获取小周期买卖点
element_all_trade_points = element_analysis['trade_points']
# 添加小周期数据到当前时间点
current_data.update({
'element_timeframe': element_timeframe,
'element_macd': element_macd_data,
'element_bollinger': {
'upper': element_subset['bb_upper'].tolist(),
'middle': element_subset['bb_middle'].tolist(),
'lower': element_subset['bb_lower'].tolist()
},
'element_element_bollinger': {
'upper': element_subset['element_bb_upper'].tolist(),
'middle': element_subset['element_bb_middle'].tolist(),
'lower': element_subset['element_bb_lower'].tolist()
},
'element_bi_list': [{
'start_time': format_time_safely(bi.begin_klc.time_end, client_tz),
'end_time': format_time_safely(bi.end_klc.time_end, client_tz) if bi.end_klc else None,
'start_price': bi.begin_klc.low if convert_direction(bi.dir) == 1 else bi.begin_klc.high,
'end_price': bi.end_klc.high if convert_direction(bi.dir) == 1 else bi.end_klc.low if bi.end_klc else None,
'direction': convert_direction(bi.dir),
'macd_div': float(element_bi_macd_divs.get(idx, 0))
} for idx, bi in enumerate(element_analysis['bi_list']) if bi.end_klc],
'element_kline_data': clean_dataframe_for_json(element_subset).to_dict('records'),
'element_seg_list': [{
'start_time': format_time_safely(seg.start_bi.begin_klc.time_end, client_tz),
'end_time': format_time_safely(seg.end_bi.end_klc.time_end, client_tz) if seg.end_bi else None,
'start_price': seg.start_bi.begin_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.begin_klc.high,
'end_price': seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low if seg.end_bi else None,
'direction': convert_direction(seg.dir)
} for seg in element_analysis['seg_list'] if seg.end_bi],
'element_zs_list': [{
'start_time': format_time_safely(zs.begin.time, client_tz),
'end_time': format_time_safely(zs.end.time, client_tz) if zs.end else None,
'zg': zs.high,
'zd': zs.low,
'is_sure': zs.is_sure
} for zs in element_analysis['zs_list'] if zs.end],
'element_uncompleted_zs_list': [{
'start_time': format_time_safely(zs.begin.time, client_tz),
'end_time': None,
'zg': zs.high,
'zd': zs.low,
'is_sure': zs.is_sure
} for zs in element_analysis['zs_list'] if not zs.is_sure],
'element_trade_points': [{
'type': point['type'],
'time': format_time_safely(point['time'], client_tz),
'price': point['price'],
'desc': point['desc']
} for point in element_all_trade_points],
'element_klc_fx_info': [{
'time': format_time_safely(point['time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']),
'fx_strength_level': str(point['fx_strength_level']),
'is_strong_fx': bool(point['is_strong_fx'])
} for point in element_analysis['klc_fx_info']],
'element_klu_fx_info': [{
'time': format_time_safely(point['time'], client_tz),
'price': float(point['price']),
'fx_type': point['fx_type'],
'is_bottom': bool(point['is_bottom']),
'fx_strength': float(point['fx_strength']),
'fx_strength_level': str(point['fx_strength_level']),
'is_strong_fx': bool(point['is_strong_fx']),
'fx_confirmed': bool(point['fx_confirmed'])
} for point in element_analysis['klu_fx_info']]
})
replay_data[i] = current_data
# 打印进度(减少频率)
if (i + 1) % 100 == 0 or i == len(df) - 1:
percent = int((i + 1) / len(df) * 100)
print(f"回放数据生成进度: {percent}% ({i + 1}/{len(df)})")
result = {
'replay_data': replay_data,
'total_length': len(df),
'timezone': client_timezone
}
print(f"回放数据生成完成: {len(df)} 条记录")
return jsonify(result)
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=8120)
+382 -262
View File
@@ -3191,22 +3191,38 @@
tvWidget.series.histogramSeries.setData(histogramData);
}
// 重新显示笔、线段和中枢等图形
redrawFractalElements();
// 恢复之前的可视范围
if (tvWidget.mainChart) {
if (tvWidget.state.logicalRange) {
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);
} 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.mainChart && (tvWidget.state.logicalRange || tvWidget.state.visibleRange)) {
console.log('回放模式:优先恢复视图范围以避免跳跃');
// 在回放模式下,图表已锁定,不设置范围
if (isReplaying) {
// 回放模式下跳过范围设置,因为图表已锁定
console.log('回放模式:跳过范围设置,图表已锁定');
} else {
// 非回放模式下延迟应用
setTimeout(() => {
if (tvWidget.state.logicalRange) {
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);
} 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);
}
}, 20);
}
}
// 重新显示笔、线段和中枢等图形
if (isReplaying) {
// 回放模式下,直接redraw,图表已锁定
redrawFractalElements();
} else {
redrawFractalElements();
}
console.log('增量更新图表完成');
} catch (e) {
console.error('增量更新图表错误,回退到完全重绘:', e);
@@ -4319,25 +4335,44 @@
// 调用初始化函数
initTradingView($('#symbol').val(), $('#timeframe').val());
// 恢复原始可见范围
setTimeout(() => {
if (tvWidget && tvWidget.mainChart) {
console.log('恢复图表可见范围...');
if (logicalRange) {
tvWidget.mainChart.timeScale().setVisibleLogicalRange(logicalRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleLogicalRange(logicalRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleLogicalRange(logicalRange);
} else if (visibleRange) {
tvWidget.mainChart.timeScale().setVisibleRange(visibleRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleRange(visibleRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(visibleRange);
// 在回放模式下立即恢复范围,避免跳跃效果
if (isReplaying) {
// 回放模式下立即设置范围,减少延迟
setTimeout(() => {
if (tvWidget && tvWidget.mainChart) {
console.log('回放模式:立即恢复图表可见范围...');
if (logicalRange) {
tvWidget.mainChart.timeScale().setVisibleLogicalRange(logicalRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleLogicalRange(logicalRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleLogicalRange(logicalRange);
} else if (visibleRange) {
tvWidget.mainChart.timeScale().setVisibleRange(visibleRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleRange(visibleRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(visibleRange);
}
console.log('图表可见范围已恢复(回放模式)');
}
console.log('图表可见范围已恢复');
} else {
console.error('恢复图表可见范围失败 - 图表未初始化');
}
}, 200);
}, 50); // 回放模式下减少延迟到50ms
} else {
// 非回放模式下保持原有的延迟
setTimeout(() => {
if (tvWidget && tvWidget.mainChart) {
console.log('正常模式:恢复图表可见范围...');
if (logicalRange) {
tvWidget.mainChart.timeScale().setVisibleLogicalRange(logicalRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleLogicalRange(logicalRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleLogicalRange(logicalRange);
} else if (visibleRange) {
tvWidget.mainChart.timeScale().setVisibleRange(visibleRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleRange(visibleRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(visibleRange);
}
console.log('图表可见范围已恢复(正常模式)');
} else {
console.error('恢复图表可见范围失败 - 图表未初始化');
}
}, 200);
}
console.log('重绘分形元素 - 完成');
}
@@ -4617,7 +4652,7 @@
const elementTimeframe = $('#elementTimeframe').val() || '1m';
// 显示回放状态
$('#replayStatus').text('正在加载数据...');
$('#replayStatus').text('正在生成回放数据...');
$('#replayProgress').show();
$('.progress-bar').css('width', '0%');
@@ -4625,62 +4660,51 @@
$('#startReplay').hide();
$('#stopReplay').show();
// 尝试使用当前已有数据
if (currentData && currentData.kline_data && currentData.kline_data.length > 0) {
const currentStart = new Date(currentData.kline_data[0].date).getTime();
const currentEnd = new Date(currentData.kline_data[currentData.kline_data.length - 1].date).getTime();
// 检查当前数据是否覆盖所需的时间范围
if (currentStart <= replayStartTime && currentEnd >= replayEndTime) {
console.log('使用当前数据进行回放,无需重新请求');
replayData = currentData;
initReplay();
return;
}
}
// 请求整个时间范围的数据
// 使用新的回放数据API
$.ajax({
url: '/api/analyze',
url: '/api/replay_data',
data: {
symbol: symbol,
timeframe: timeframe,
timezone: timezone,
element_timeframe: elementTimeframe,
start_time: replayStartTime,
end_time: replayEndTime,
elements_only: false
end_time: replayEndTime
},
success: function(data) {
// 检查是否有有效数据
if (!data || !data.kline_data || data.kline_data.length === 0) {
$('#replayStatus').text('服务器返回的数据无效');
if (!data || !data.replay_data || Object.keys(data.replay_data).length === 0) {
$('#replayStatus').text('服务器返回的回放数据无效');
$('#replayProgress').hide();
$('#startReplay').show();
$('#stopReplay').hide();
console.error('服务器返回的数据无效:', data);
console.error('服务器返回的回放数据无效:', data);
return;
}
// 保存完整数据
replayData = data;
// 保存回放数据
replayData = data.replay_data;
totalReplaySteps = data.total_length;
console.log('回放数据加载完成,总步骤数:', totalReplaySteps);
console.log('回放数据已从后台预计算完成,确保不会使用未来数据');
// 初始化回放
initReplay();
},
error: function(jqXHR, textStatus, errorThrown) {
$('#replayStatus').text('加载数据失败');
$('#replayStatus').text('生成回放数据失败');
$('#replayProgress').hide();
$('#startReplay').show();
$('#stopReplay').hide();
alert('加载数据失败: ' + (jqXHR.responseJSON?.error || errorThrown));
alert('生成回放数据失败: ' + (jqXHR.responseJSON?.error || errorThrown));
}
});
}
// 初始化回放
function initReplay() {
if (!replayData || !replayData.kline_data || replayData.kline_data.length === 0) {
if (!replayData || Object.keys(replayData).length === 0) {
$('#replayStatus').text('没有可回放的数据');
$('#replayProgress').hide();
$('#startReplay').show();
@@ -4691,98 +4715,208 @@
// 设置回放变量
isReplaying = true;
currentReplayIndex = 0;
totalReplaySteps = replayData.kline_data.length;
// totalReplaySteps已经在startDataReplay中设置了
// 更新回放状态
$('#replayStatus').text(`准备回放 (0/${totalReplaySteps})`);
$('.progress-bar').css('width', '0%');
// 准备初始数据
prepareInitialReplayData();
// 预先锁定图表,防止任何自动调整
lockChartsForReplay();
// 准备初始数据(显示第一个时间点的数据)
if (replayData[0]) {
currentData = replayData[0];
updateTradingViewData();
updateTables(currentData);
}
// 开始回放
continueDataReplay();
}
// 准备初始回放数据
function prepareInitialReplayData() {
// 创建初始数据集,只包含第一根K线
const initialData = $.extend(true, {}, replayData);
// 为回放优化图表设置,减少跳跃效果
function optimizeChartForReplay() {
if (!tvWidget || !tvWidget.mainChart) return;
// 确保第一根K线数据有效
if (!replayData.kline_data || replayData.kline_data.length === 0) {
console.error('无有效K线数据用于回放');
$('#replayStatus').text('无有效数据用于回放');
$('#replayProgress').hide();
return;
}
initialData.kline_data = [replayData.kline_data[0]];
// 确保初始K线数据的所有字段都有值
const firstKline = initialData.kline_data[0];
if (firstKline) {
// 确保K线数据的每个字段都是有效的数值
['open', 'high', 'low', 'close', 'volume'].forEach(field => {
if (firstKline[field] === null || firstKline[field] === undefined || isNaN(firstKline[field])) {
console.warn(`K线数据的${field}字段无效,设置为默认值`);
firstKline[field] = field === 'volume' ? 0 : firstKline.close || 0;
}
});
}
// 清空其他数据列表,因为还没有处理
initialData.bi_list = [];
initialData.seg_list = [];
initialData.zs_list = [];
initialData.uncompleted_zs_list = [];
initialData.element_bi_list = [];
initialData.element_seg_list = [];
initialData.element_zs_list = [];
initialData.element_uncompleted_zs_list = [];
initialData.macd_divergence = [];
initialData.klc_fx_type = [];
// 初始化MACD数据
if (initialData.macd && initialData.macd.length > 0) {
initialData.macd = [initialData.macd[0]];
try {
console.log('优化图表设置以减少回放跳跃...');
// 确保MACD数据有效
const firstMacd = initialData.macd[0];
if (firstMacd) {
['dif', 'dea', 'macd'].forEach(field => {
if (firstMacd[field] === null || firstMacd[field] === undefined || isNaN(firstMacd[field])) {
console.warn(`MACD数据的${field}字段无效,设置为0`);
firstMacd[field] = 0;
// 临时禁用动画,提高性能
if (tvWidget.mainChart.applyOptions) {
tvWidget.mainChart.applyOptions({
timeScale: {
rightOffset: 5, // 减少右侧偏移
barSpacing: 6, // 固定K线间距
fixLeftEdge: false,
fixRightEdge: false,
lockVisibleTimeRangeOnResize: true // 窗口大小改变时锁定可视范围
}
});
}
}
// 更新图表,固定坐标轴范围
currentData = initialData;
// 清除当前图表
if (tvWidget.state.isInitialized) {
// 保存当前的图表可见范围以供后续使用
try {
tvWidget.state.visibleRange = tvWidget.mainChart.timeScale().getVisibleRange();
tvWidget.state.logicalRange = tvWidget.mainChart.timeScale().getVisibleLogicalRange();
} catch (e) {
console.error('保存图表范围失败', e);
if (tvWidget.volumeChart && tvWidget.volumeChart.applyOptions) {
tvWidget.volumeChart.applyOptions({
timeScale: {
rightOffset: 5,
barSpacing: 6,
lockVisibleTimeRangeOnResize: true
}
});
}
if (tvWidget.macdChart && tvWidget.macdChart.applyOptions) {
tvWidget.macdChart.applyOptions({
timeScale: {
rightOffset: 5,
barSpacing: 6,
lockVisibleTimeRangeOnResize: true
}
});
}
console.log('图表优化设置完成');
} catch (e) {
console.error('优化图表设置失败:', e);
}
// 刷新图表
refreshChart(initialData);
// 固定y轴范围
fixYAxisRange();
}
// 锁定图表,阻止所有自动调整行为
function lockChartsForReplay() {
try {
if (tvWidget && tvWidget.mainChart) {
// 完全禁用自动缩放和范围调整
tvWidget.mainChart.applyOptions({
handleScroll: false,
handleScale: false,
timeScale: {
rightOffset: 0,
fixLeftEdge: true,
fixRightEdge: true,
lockVisibleTimeRangeOnResize: true,
borderVisible: false
},
priceScale: {
autoScale: false,
borderVisible: false
}
});
if (tvWidget.volumeChart) {
tvWidget.volumeChart.applyOptions({
handleScroll: false,
handleScale: false,
timeScale: {
rightOffset: 0,
fixLeftEdge: true,
fixRightEdge: true,
lockVisibleTimeRangeOnResize: true,
borderVisible: false
},
priceScale: {
autoScale: false,
borderVisible: false
}
});
}
if (tvWidget.macdChart) {
tvWidget.macdChart.applyOptions({
handleScroll: false,
handleScale: false,
timeScale: {
rightOffset: 0,
fixLeftEdge: true,
fixRightEdge: true,
lockVisibleTimeRangeOnResize: true,
borderVisible: false
},
priceScale: {
autoScale: false,
borderVisible: false
}
});
}
console.log('图表已锁定,禁用所有自动调整');
}
} catch (e) {
console.error('锁定图表失败:', e);
}
}
// 解锁图表,恢复正常交互
function unlockChartsAfterReplay() {
try {
if (tvWidget && tvWidget.mainChart) {
// 恢复正常的交互和自动调整
tvWidget.mainChart.applyOptions({
handleScroll: true,
handleScale: true,
timeScale: {
rightOffset: 12,
fixLeftEdge: false,
fixRightEdge: false,
lockVisibleTimeRangeOnResize: false,
borderVisible: true
},
priceScale: {
autoScale: true,
borderVisible: true
}
});
if (tvWidget.volumeChart) {
tvWidget.volumeChart.applyOptions({
handleScroll: true,
handleScale: true,
timeScale: {
rightOffset: 12,
fixLeftEdge: false,
fixRightEdge: false,
lockVisibleTimeRangeOnResize: false,
borderVisible: true
},
priceScale: {
autoScale: true,
borderVisible: true
}
});
}
if (tvWidget.macdChart) {
tvWidget.macdChart.applyOptions({
handleScroll: true,
handleScale: true,
timeScale: {
rightOffset: 12,
fixLeftEdge: false,
fixRightEdge: false,
lockVisibleTimeRangeOnResize: false,
borderVisible: true
},
priceScale: {
autoScale: true,
borderVisible: true
}
});
}
console.log('图表已解锁,恢复正常交互');
}
} catch (e) {
console.error('解锁图表失败:', e);
}
}
// 固定y轴范围,保持图表不因数据变化而变化
function fixYAxisRange() {
if (!tvWidget.mainChart) return;
if (!tvWidget.mainChart || !replayData || !replayData.kline_data || replayData.kline_data.length === 0) {
console.log('无法固定Y轴范围:图表或数据不可用');
return;
}
// 获取全部数据的最高价和最低价,为了适当的范围
let minPrice = Infinity;
@@ -4793,10 +4927,16 @@
const high = parseFloat(kline.high);
const low = parseFloat(kline.low);
if (high > maxPrice) maxPrice = high;
if (low < minPrice) minPrice = low;
if (!isNaN(high) && high > maxPrice) maxPrice = high;
if (!isNaN(low) && low < minPrice) minPrice = low;
});
// 检查是否获得了有效的价格范围
if (minPrice === Infinity || maxPrice === -Infinity) {
console.warn('无法获得有效的价格范围,跳过Y轴固定');
return;
}
// 添加一些边距(约5%)
const priceRange = maxPrice - minPrice;
maxPrice += priceRange * 0.05;
@@ -4815,6 +4955,7 @@
minimumValue: minPrice,
maximumValue: maxPrice
});
console.log(`Y轴范围已固定: ${minPrice.toFixed(2)} - ${maxPrice.toFixed(2)}`);
} catch (e) {
console.error('设置Y轴范围失败:', e);
}
@@ -4847,164 +4988,104 @@
currentReplayIndex++;
updateReplayStatus();
// 创建截止到当前索引的数据子集
const subsetData = createDataSubset(currentReplayIndex);
// 直接从后台预计算的数据中取出当前时间点的数据
const currentStepData = replayData[currentReplayIndex - 1]; // 索引从0开始
// 更新图表
currentData = subsetData;
refreshChart(subsetData);
if (!currentStepData) {
console.error(`回放步骤 ${currentReplayIndex} 没有数据`);
return;
}
// 固定Y轴范围
fixYAxisRange();
// 更新当前数据
currentData = currentStepData;
// 保持最新数据可见
keepLatestDataVisible();
// 直接更新数据,图表已锁定不会跳跃
console.log(`回放步骤 ${currentReplayIndex}/${totalReplaySteps}:使用预计算数据`);
try {
updateTradingViewData();
// 表格更新频率降低,每20步更新一次
if (currentReplayIndex % 20 === 0 || currentReplayIndex === totalReplaySteps) {
updateTables(currentData);
}
// 不再需要频繁的Y轴调整和视图调整,因为图表已锁定
} catch (e) {
console.error('回放步骤更新失败:', e);
}
// 如果到达最后一步,停止回放
if (currentReplayIndex >= totalReplaySteps) {
$('#replayStatus').text('回放完成');
// 回放完成时解锁图表
unlockChartsAfterReplay();
pauseDataReplay();
}
}
// 保持最新数据可见
function keepLatestDataVisible() {
if (!tvWidget.mainChart) return;
if (!tvWidget.mainChart || !currentData || !currentData.kline_data || currentData.kline_data.length === 0) return;
try {
// 获取当前数据的最后一个时间点
if (currentData && currentData.kline_data && currentData.kline_data.length > 0) {
const lastBar = currentData.kline_data[currentData.kline_data.length - 1];
const lastTime = Math.floor(new Date(lastBar.date).getTime() / 1000);
// 在回放模式下,更平滑地调整视图范围
const lastBar = currentData.kline_data[currentData.kline_data.length - 1];
const lastTime = Math.floor(new Date(lastBar.date).getTime() / 1000);
// 如果已经有保存的视图范围,则基于它进行微调,避免大幅跳跃
if (tvWidget.state.logicalRange) {
const currentLogicalRange = tvWidget.mainChart.timeScale().getVisibleLogicalRange();
const currentTimeRange = tvWidget.mainChart.timeScale().getVisibleRange();
// 计算时间范围
const barCount = Math.min(50, currentData.kline_data.length); // 显示最近的50根K线或所有K线
if (currentTimeRange && currentTimeRange.to) {
// 检查最新数据是否已经在可视范围内
const timeDiff = lastTime - currentTimeRange.to;
// 只有当最新数据超出当前可视范围的一定程度时才调整
if (timeDiff > 0) {
const barCount = Math.min(50, currentData.kline_data.length);
const firstVisibleIndex = Math.max(0, currentData.kline_data.length - barCount);
const firstVisibleBar = currentData.kline_data[firstVisibleIndex];
const firstTime = Math.floor(new Date(firstVisibleBar.date).getTime() / 1000);
// 平滑地扩展视图范围,而不是突然跳跃
const newRange = {
from: Math.min(currentTimeRange.from || firstTime, firstTime),
to: lastTime
};
tvWidget.mainChart.timeScale().setVisibleRange(newRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleRange(newRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(newRange);
console.log('平滑调整视图范围:', newRange);
}
}
} else {
// 初始情况,设置默认的视图范围
const barCount = Math.min(50, currentData.kline_data.length);
const firstVisibleIndex = Math.max(0, currentData.kline_data.length - barCount);
const firstVisibleBar = currentData.kline_data[firstVisibleIndex];
const firstTime = Math.floor(new Date(firstVisibleBar.date).getTime() / 1000);
// 设置时间轴范围
tvWidget.mainChart.timeScale().setVisibleRange({
const newRange = {
from: firstTime,
to: lastTime
});
};
tvWidget.mainChart.timeScale().setVisibleRange(newRange);
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleRange(newRange);
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(newRange);
console.log('设置初始视图范围:', newRange);
}
} catch (e) {
console.error('保持最新数据可见失败:', e);
}
}
// 创建数据子集
function createDataSubset(endIndex) {
// 创建数据副本
const subsetData = $.extend(true, {}, replayData);
// 截取K线数据
subsetData.kline_data = replayData.kline_data.slice(0, endIndex);
// 验证K线数据
subsetData.kline_data.forEach((kline, index) => {
['open', 'high', 'low', 'close', 'volume'].forEach(field => {
if (kline[field] === null || kline[field] === undefined || isNaN(kline[field])) {
console.warn(`K线数据[${index}]的${field}字段无效,使用替代值`);
// 对于价格字段,尝试使用其他价格字段,或者前一个K线的对应值
if (field !== 'volume') {
if (field === 'open' && kline.close !== null && !isNaN(kline.close)) {
kline[field] = kline.close;
} else if (index > 0) {
kline[field] = subsetData.kline_data[index-1][field] || 0;
} else {
kline[field] = 0;
}
} else {
kline[field] = 0; // 对于成交量,设为0
}
}
});
});
// 处理其他数据结构(笔、线段、中枢等)
// 我们需要根据当前K线的时间来过滤其他数据结构
try {
const currentEndTime = new Date(subsetData.kline_data[endIndex - 1].date).getTime();
// 过滤笔列表
if (subsetData.bi_list && subsetData.bi_list.length) {
subsetData.bi_list = filterDataByTime(replayData.bi_list, currentEndTime);
}
// 过滤线段列表
if (subsetData.seg_list && subsetData.seg_list.length) {
subsetData.seg_list = filterDataByTime(replayData.seg_list, currentEndTime);
}
// 过滤中枢列表
if (subsetData.zs_list && subsetData.zs_list.length) {
subsetData.zs_list = filterDataByTime(replayData.zs_list, currentEndTime);
}
// 过滤未完成中枢列表
if (subsetData.uncompleted_zs_list && subsetData.uncompleted_zs_list.length) {
subsetData.uncompleted_zs_list = filterDataByTime(replayData.uncompleted_zs_list, currentEndTime);
}
// 过滤小周期数据
if (subsetData.element_bi_list && subsetData.element_bi_list.length) {
subsetData.element_bi_list = filterDataByTime(replayData.element_bi_list, currentEndTime);
}
if (subsetData.element_seg_list && subsetData.element_seg_list.length) {
subsetData.element_seg_list = filterDataByTime(replayData.element_seg_list, currentEndTime);
}
if (subsetData.element_zs_list && subsetData.element_zs_list.length) {
subsetData.element_zs_list = filterDataByTime(replayData.element_zs_list, currentEndTime);
}
if (subsetData.element_uncompleted_zs_list && subsetData.element_uncompleted_zs_list.length) {
subsetData.element_uncompleted_zs_list = filterDataByTime(replayData.element_uncompleted_zs_list, currentEndTime);
}
// 过滤MACD
if (subsetData.macd && subsetData.macd.length) {
subsetData.macd = replayData.macd.slice(0, endIndex);
// 验证MACD数据
subsetData.macd.forEach((item, index) => {
['dif', 'dea', 'macd'].forEach(field => {
if (item[field] === null || item[field] === undefined || isNaN(item[field])) {
console.warn(`MACD数据[${index}]的${field}字段无效,设置为0`);
item[field] = 0;
}
});
});
}
// 过滤MACD背离
if (subsetData.macd_divergence && subsetData.macd_divergence.length) {
subsetData.macd_divergence = filterDataByTime(replayData.macd_divergence, currentEndTime);
}
// 过滤分型类型
if (subsetData.klc_fx_type && subsetData.klc_fx_type.length) {
subsetData.klc_fx_type = filterDataByTime(replayData.klc_fx_type, currentEndTime, 'time');
}
} catch (e) {
console.error('过滤数据时出错:', e);
}
return subsetData;
}
// 根据时间过滤数据
function filterDataByTime(dataList, endTime, timeField = 'end_time') {
if (!dataList || !Array.isArray(dataList)) return [];
return dataList.filter(item => {
const itemTime = new Date(item[timeField]).getTime();
return itemTime <= endTime;
});
}
// 更新回放状态显示
function updateReplayStatus() {
@@ -5044,6 +5125,9 @@
currentReplayIndex = 0;
replayData = null;
// 清空当前数据,确保下次回放是干净状态
currentData = null;
// 更新UI
$('#replayStatus').text('');
$('#replayProgress').hide();
@@ -5051,12 +5135,48 @@
$('#stopReplay').hide();
$('#startReplay').show();
// 恢复图表自动缩放
// 恢复图表自动缩放和正常设置
if (tvWidget.mainChart) {
tvWidget.mainChart.priceScale().applyOptions({
autoScale: true
});
// 恢复图表的正常设置
tvWidget.mainChart.applyOptions({
timeScale: {
rightOffset: 12, // 恢复默认右侧偏移
barSpacing: 6,
fixLeftEdge: false,
fixRightEdge: false,
lockVisibleTimeRangeOnResize: false // 恢复窗口调整时的自动适应
}
});
if (tvWidget.volumeChart) {
tvWidget.volumeChart.applyOptions({
timeScale: {
rightOffset: 12,
barSpacing: 6,
lockVisibleTimeRangeOnResize: false
}
});
}
if (tvWidget.macdChart) {
tvWidget.macdChart.applyOptions({
timeScale: {
rightOffset: 12,
barSpacing: 6,
lockVisibleTimeRangeOnResize: false
}
});
}
}
// 解锁图表,恢复正常交互
unlockChartsAfterReplay();
console.log('回放已停止,数据已清空,图表已解锁');
}
// 获取A股股票列表