diff --git a/web/app.py b/web/app.py index 4ae27be..2513ebf 100644 --- a/web/app.py +++ b/web/app.py @@ -353,6 +353,112 @@ def analyze_chan(df): for bi in bi_list: bi.cal_macd_div() #print(bi.start_time, bi.macd_hist, bi.macd_div) + +def generate_replay_data(df, client_tz): + """生成逐步计算的回放数据""" + print(f"开始生成回放数据,K线总数: {len(df)}") + + replay_data = {} + + # 为每个K线索引计算分析结果 + for i in range(1, len(df) + 1): # 从1开始,至少需要1根K线 + try: + # 截取到当前索引的数据 + current_df = df.iloc[:i].copy() + + # 添加技术指标 + current_df = add_indicators(current_df) + + # 进行缠论分析 + analysis_result = analyze_chan(current_df) + + # 计算MACD + macd_data = calculate_macd(current_df) + + # 构建该索引对应的分析结果 + step_data = { + 'kline_data': clean_dataframe_for_json(current_df).to_dict('records'), + 'bi_list': [{ + 'start_time': bi.start_klc.end_time if isinstance(bi.start_klc.end_time, str) else bi.start_klc.end_time.astimezone(client_tz).isoformat(), + 'end_time': (bi.end_klc.end_time if isinstance(bi.end_klc.end_time, str) else bi.end_klc.end_time.astimezone(client_tz).isoformat()) if bi.end_klc else None, + 'sure_time': format_time_safely(bi.sure_time, client_tz) if bi.sure_time else None, + 'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_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_div) if hasattr(bi, 'macd_div') else 0 + } for bi in analysis_result['bi_list'] if bi.end_klc], + 'seg_list': [{ + 'start_time': seg.start_bi.start_klc.end_time if isinstance(seg.start_bi.start_klc.end_time, str) else seg.start_bi.start_klc.end_time.astimezone(client_tz).isoformat(), + 'end_time': (seg.end_bi.end_klc.end_time if isinstance(seg.end_bi.end_klc.end_time, str) else seg.end_bi.end_klc.end_time.astimezone(client_tz).isoformat()) if seg.end_bi else None, + 'sure_time': format_time_safely(seg.sure_time, client_tz) if seg.sure_time else None, + 'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_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': zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat(), + 'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None, + 'zg': zs.zg, + 'zd': zs.zd, + 'is_sure': zs.is_sure + } for zs in analysis_result['zs_list'] if zs.end_klc], + 'uncompleted_zs_list': [{ + 'start_time': zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat(), + 'end_time': None, + 'zg': zs.zg, + 'zd': zs.zd, + '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 analysis_result['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']] + } + + replay_data[i-1] = step_data # 使用0-based索引 + + # 每处理100个点输出一次进度 + if i % 100 == 0 or i == len(df): + print(f"生成回放数据进度: {i}/{len(df)}") + + except Exception as e: + print(f"生成第{i}步回放数据时出错: {e}") + continue + + print(f"回放数据生成完成,总步数: {len(replay_data)}") + return replay_data # 获取原始K线数据用于KLU分型分析 klu_list = [] @@ -822,9 +928,13 @@ def analyze(): elements_only_param = request.args.get('elements_only') elements_only = elements_only_param == 'true' + # 获取是否需要回放数据的参数 + need_replay_data = request.args.get('need_replay_data', 'false').lower() == 'true' + print(f"API请求参数: symbol={symbol}, timeframe={timeframe}, element_timeframe={element_timeframe}") print(f"时间范围: start_time={start_time}, end_time={end_time}") print(f"elements_only参数: 原始值={elements_only_param}, 处理后={elements_only}") + print(f"need_replay_data参数: {need_replay_data}") # 验证小周期是否小于主周期 if element_timeframe and not is_smaller_or_equal_timeframe(element_timeframe, timeframe): @@ -862,6 +972,14 @@ def analyze(): # 计算MACD macd_data = calculate_macd(df) + # 如果需要回放数据,生成逐步计算的回放数据 + if need_replay_data: + print("开始生成回放数据...") + replay_data = generate_replay_data(df, client_tz) + print(f"回放数据生成完成,包含 {len(replay_data)} 个步骤") + else: + replay_data = None + # 添加主周期分析结果到返回数据 result.update({ 'kline_data': clean_dataframe_for_json(df).to_dict('records'), @@ -936,6 +1054,10 @@ def analyze(): 'fx_confirmed': bool(point['fx_confirmed']) # 分型是否确认 } for point in analysis_result['klu_fx_info']] }) + + # 如果生成了回放数据,添加到返回结果中 + if replay_data is not None: + result['replay_data'] = replay_data else: print(f"只请求元素数据,跳过主周期数据处理 (elements_only={elements_only})") diff --git a/web/templates/index.html b/web/templates/index.html index aaadd66..ebe76fc 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -5288,6 +5288,7 @@ // 数据回放相关变量 let replayTimer = null; let replayData = null; + let replayStepData = null; // 存储逐步计算的回放数据 let currentReplayIndex = 0; let totalReplaySteps = 0; let isReplaying = false; @@ -5373,7 +5374,7 @@ } } - // 请求整个时间范围的数据 + // 请求整个时间范围的数据,包含回放数据 $.ajax({ url: '/api/analyze', data: { @@ -5383,7 +5384,8 @@ element_timeframe: elementTimeframe, start_time: replayStartTime, end_time: replayEndTime, - elements_only: false + elements_only: false, + need_replay_data: true // 请求逐步计算的回放数据 }, success: function(data) { // 检查是否有有效数据 @@ -5414,7 +5416,24 @@ // 初始化回放 function initReplay() { - if (!replayData || !replayData.kline_data || replayData.kline_data.length === 0) { + // 检查是否有新的回放数据结构 + if (replayData && replayData.replay_data) { + console.log('使用新的逐步计算回放数据'); + replayStepData = replayData.replay_data; // 存储逐步计算的数据 + totalReplaySteps = Object.keys(replayStepData).length; + + if (totalReplaySteps === 0) { + $('#replayStatus').text('没有可回放的数据'); + $('#replayProgress').hide(); + $('#startReplay').show(); + $('#stopReplay').hide(); + return; + } + } else if (replayData && replayData.kline_data && replayData.kline_data.length > 0) { + console.log('使用传统回放数据结构'); + replayStepData = null; // 标记使用传统方式 + totalReplaySteps = replayData.kline_data.length; + } else { $('#replayStatus').text('没有可回放的数据'); $('#replayProgress').hide(); $('#startReplay').show(); @@ -5425,7 +5444,6 @@ // 设置回放变量 isReplaying = true; currentReplayIndex = 0; - totalReplaySteps = replayData.kline_data.length; // 更新回放状态 $('#replayStatus').text(`准备回放 (0/${totalReplaySteps})`); @@ -5440,59 +5458,90 @@ // 准备初始回放数据 function prepareInitialReplayData() { - // 创建初始数据集,只包含第一根K线 - const initialData = $.extend(true, {}, replayData); + let initialData; - // 确保第一根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]]; + // 检查是否使用新的逐步计算回放数据 + if (replayStepData) { + // 使用第一步的数据作为初始数据 + initialData = $.extend(true, {}, replayStepData[0]); + console.log('使用逐步计算的初始数据'); + } else { + // 使用传统方式,创建初始数据集,只包含第一根K线 + initialData = $.extend(true, {}, replayData); - // 确保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; + // 确保第一根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.trade_points = []; + initialData.element_trade_points = []; + initialData.macd_divergence = []; + initialData.klc_fx_type = []; + // 清空分型相关数据 + initialData.klc_fx_info = []; + initialData.klu_fx_info = []; + initialData.element_klc_fx_info = []; + initialData.element_klu_fx_info = []; + + // 初始化MACD数据 + if (initialData.macd && typeof initialData.macd === 'object') { + // MACD数据是对象结构 {macd: [], signal: [], histogram: []} + if (initialData.macd.macd && initialData.macd.macd.length > 0) { + initialData.macd = { + macd: [initialData.macd.macd[0] || 0], + signal: [initialData.macd.signal[0] || 0], + histogram: [initialData.macd.histogram[0] || 0] + }; + } else { + // 如果没有MACD数据,创建默认值 + initialData.macd = { + macd: [0], + signal: [0], + histogram: [0] + }; + } + } else { + // 如果MACD数据结构不正确,创建默认值 + initialData.macd = { + macd: [0], + signal: [0], + histogram: [0] + }; + } + + console.log('使用传统方式生成初始数据'); } + + // 更新图表,固定坐标轴范围 currentData = initialData; @@ -5507,6 +5556,16 @@ } } + // 输出调试信息,确认数据已正确清空 + console.log('回放初始化完成,数据统计:'); + console.log('K线数据:', initialData.kline_data ? initialData.kline_data.length : 0, '条'); + console.log('笔数据:', initialData.bi_list ? initialData.bi_list.length : 0, '条'); + console.log('线段数据:', initialData.seg_list ? initialData.seg_list.length : 0, '条'); + console.log('中枢数据:', initialData.zs_list ? initialData.zs_list.length : 0, '条'); + console.log('KLC分型数据:', initialData.klc_fx_info ? initialData.klc_fx_info.length : 0, '条'); + console.log('KLU分型数据:', initialData.klu_fx_info ? initialData.klu_fx_info.length : 0, '条'); + console.log('买卖点数据:', initialData.trade_points ? initialData.trade_points.length : 0, '条'); + // 刷新图表 refreshChart(initialData); @@ -5581,15 +5640,35 @@ currentReplayIndex++; updateReplayStatus(); - // 创建截止到当前索引的数据子集 - const subsetData = createDataSubset(currentReplayIndex); + let subsetData; + + // 检查是否使用新的逐步计算数据 + if (replayStepData) { + // 直接使用预计算的数据 + subsetData = $.extend(true, {}, replayStepData[currentReplayIndex - 1]); + console.log(`回放步骤 ${currentReplayIndex}/${totalReplaySteps} (使用预计算数据)`); + } else { + // 使用传统方式创建数据子集 + subsetData = createDataSubset(currentReplayIndex); + console.log(`回放步骤 ${currentReplayIndex}/${totalReplaySteps} (传统方式)`); + } + + // 输出回放进度调试信息 + if (currentReplayIndex % 10 === 0 || currentReplayIndex <= 5) { // 每10步输出一次,或前5步 + console.log('K线数据:', subsetData.kline_data ? subsetData.kline_data.length : 0, '条'); + console.log('笔数据:', subsetData.bi_list ? subsetData.bi_list.length : 0, '条'); + console.log('KLC分型:', subsetData.klc_fx_info ? subsetData.klc_fx_info.length : 0, '条'); + console.log('KLU分型:', subsetData.klu_fx_info ? subsetData.klu_fx_info.length : 0, '条'); + } // 更新图表 currentData = subsetData; refreshChart(subsetData); // 固定Y轴范围 - fixYAxisRange(); + if (!replayStepData) { // 只有传统方式需要固定Y轴 + fixYAxisRange(); + } // 保持最新数据可见 keepLatestDataVisible(); @@ -5699,19 +5778,32 @@ 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 && typeof subsetData.macd === 'object') { + if (replayData.macd.macd && replayData.macd.macd.length > 0) { + subsetData.macd = { + macd: replayData.macd.macd.slice(0, endIndex), + signal: replayData.macd.signal.slice(0, endIndex), + histogram: replayData.macd.histogram.slice(0, endIndex) + }; + + // 验证MACD数据 + ['macd', 'signal', 'histogram'].forEach(field => { + subsetData.macd[field].forEach((value, index) => { + if (value === null || value === undefined || isNaN(value)) { + console.warn(`MACD ${field}[${index}]数据无效,设置为0`); + subsetData.macd[field][index] = 0; + } + }); }); - }); + } else { + // 如果没有有效的MACD数据,创建默认数组 + subsetData.macd = { + macd: new Array(endIndex).fill(0), + signal: new Array(endIndex).fill(0), + histogram: new Array(endIndex).fill(0) + }; + } } // 过滤MACD背离 @@ -5723,6 +5815,32 @@ if (subsetData.klc_fx_type && subsetData.klc_fx_type.length) { subsetData.klc_fx_type = filterDataByTime(replayData.klc_fx_type, currentEndTime, 'time'); } + + // 过滤分型信息数据 + if (subsetData.klc_fx_info && subsetData.klc_fx_info.length) { + subsetData.klc_fx_info = filterDataByTime(replayData.klc_fx_info, currentEndTime, 'time'); + } + + if (subsetData.klu_fx_info && subsetData.klu_fx_info.length) { + subsetData.klu_fx_info = filterDataByTime(replayData.klu_fx_info, currentEndTime, 'time'); + } + + if (subsetData.element_klc_fx_info && subsetData.element_klc_fx_info.length) { + subsetData.element_klc_fx_info = filterDataByTime(replayData.element_klc_fx_info, currentEndTime, 'time'); + } + + if (subsetData.element_klu_fx_info && subsetData.element_klu_fx_info.length) { + subsetData.element_klu_fx_info = filterDataByTime(replayData.element_klu_fx_info, currentEndTime, 'time'); + } + + // 过滤买卖点数据 + if (subsetData.trade_points && subsetData.trade_points.length) { + subsetData.trade_points = filterDataByTime(replayData.trade_points, currentEndTime, 'time'); + } + + if (subsetData.element_trade_points && subsetData.element_trade_points.length) { + subsetData.element_trade_points = filterDataByTime(replayData.element_trade_points, currentEndTime, 'time'); + } } catch (e) { console.error('过滤数据时出错:', e); } @@ -5777,6 +5895,7 @@ isReplaying = false; currentReplayIndex = 0; replayData = null; + replayStepData = null; // 清理逐步计算数据 // 更新UI $('#replayStatus').text('');