From 3dc7dd532290f4b974836a72dac47ee841219537 Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Tue, 10 Jun 2025 18:01:42 +0800 Subject: [PATCH] Add replay good now --- DataAPI/WebDataAPI.py | 7 - web/app.py | 302 +++++++++++++++--- web/templates/index.html | 644 +++++++++++++++++++++++---------------- 3 files changed, 637 insertions(+), 316 deletions(-) diff --git a/DataAPI/WebDataAPI.py b/DataAPI/WebDataAPI.py index 026182e..55376e2 100644 --- a/DataAPI/WebDataAPI.py +++ b/DataAPI/WebDataAPI.py @@ -29,8 +29,6 @@ class WebDataAPI(CCommonStockApi): # 确保数据按时间排序并重置索引 df_data = df_data.sort_values('date').reset_index(drop=True) - print(f"WebDataAPI: 处理 {len(df_data)} 条K线数据,K线级别: {self.k_type}") - # 检查并处理重复时间 if 'timestamp' in df_data.columns: df_data = df_data.drop_duplicates(subset=['timestamp'], keep='last') @@ -48,7 +46,6 @@ class WebDataAPI(CCommonStockApi): if 'timestamp' in row: current_timestamp = row['timestamp'] if prev_timestamp is not None and current_timestamp <= prev_timestamp: - print(f"跳过重复或倒序时间戳: {current_timestamp}, 上个时间戳: {prev_timestamp}") continue prev_timestamp = current_timestamp @@ -72,10 +69,6 @@ class WebDataAPI(CCommonStockApi): auto=use_auto ) - # 输出详细调试信息(只输出前几条) - if idx < 3: - print(f"第{idx+1}条数据: 原始时间={time_obj}, CTime={ctime}, auto={use_auto}, timestamp={ctime.ts}") - # 创建数据字典 data_dict = { DATA_FIELD.FIELD_TIME: ctime, diff --git a/web/app.py b/web/app.py index 3af2b7b..4326039 100644 --- a/web/app.py +++ b/web/app.py @@ -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) \ No newline at end of file diff --git a/web/templates/index.html b/web/templates/index.html index 6831616..ca3a384 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -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股股票列表