"""分析 API。""" from flask import Blueprint, jsonify, request from services.runtime import * # noqa: F403 from services import runtime as R bp = Blueprint("analyze", __name__) @bp.route('/api/analyze') def analyze(): """分析接口""" symbol = request.args.get('symbol', 'SOL/USDT:USDT') timeframe = request.args.get('timeframe', '5m') # 验证交易对不为空 if not symbol or symbol.strip() == '': 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') sub_sub_timeframe = request.args.get('sub_sub_timeframe') # 获取是否只需要分形元素数据的参数 elements_only_param = request.args.get('elements_only') elements_only = elements_only_param == 'true' # 验证小周期是否小于主周期 if element_timeframe and not is_smaller_or_equal_timeframe(element_timeframe, timeframe): return jsonify({'error': '分形元素时间周期必须小于或等于主图表时间周期'}) # 验证次次周期是否小于等于次周期 if sub_sub_timeframe and element_timeframe and not is_smaller_or_equal_timeframe(sub_sub_timeframe, element_timeframe): return jsonify({'error': '次次周期必须小于或等于次周期'}) # 获取数据 df = get_kl_data(symbol, timeframe, start_time=start_time, end_time=end_time) if df is None: return jsonify({'error': '获取数据失败'}) if len(df) == 0: return jsonify({'error': '所选时间范围内没有数据'}) # 使用客户端指定的时区 client_tz = timezone(client_timezone) # 如果只需要分形元素数据而不需要主周期数据,则初始化一个空结果 result = { 'timezone': client_timezone } # 如果不是只需要分形元素数据,则添加主周期数据 if not elements_only: # 添加技术指标(包括布林带) df = add_indicators(df) # 进行缠论分析 analysis_result = analyze_chan(df, symbol, timeframe) # 计算MACD macd_data = calculate_macd(df) # 基于已有 KLC 列表生成趋势标记(不做额外计算) klc_trend = [] try: for klc in analysis_result.get('klc_list', []): trend_val = getattr(klc, 'trend', None) t_obj = getattr(klc, 'end_time', None) or getattr(klc, 'start_time', None) if trend_val is None or t_obj is None: continue # 统一成字符串:UP/DOWN/FLAT/UNKNOWN trend_name = str(trend_val) if '.' in trend_name: trend_name = trend_name.split('.')[-1] time_str = format_time_safely(t_obj, client_tz) if time_str: klc_trend.append({'time': time_str, 'trend': trend_name}) except Exception: klc_trend = [] # 添加主周期分析结果到返回数据 result.update({ 'kline_data': clean_dataframe_for_json(df).to_dict('records'), 'klc_list': [{ 'date': klc.end_time if isinstance(klc.end_time, str) else klc.end_time.astimezone(client_tz).isoformat(), 'open': float(klc.open), 'high': float(klc.high), 'low': float(klc.low), 'close': float(klc.close), 'volume': float(klc.volume) if hasattr(klc, 'volume') else 0, 'direction': str(klc.dir).replace('Chan_KLINE_DIR.', ''), 'fx_type': str(klc.fx).replace('Chan_FX_TYPE.', ''), 'klc_fx_type': str(klc.klc_fx_type).replace('Chan_KLC_FX.', ''), 'trend': str(klc.trend).replace('Chan_PRICE_TREND.', '') } for klc in analysis_result['klc_list'] if hasattr(klc, 'end_time') and klc.end_time], '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.is_sure], # 添加未完成笔列表 'uncompleted_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_time, # 未完成笔没有结束时间 '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.low if convert_direction(bi.dir) == 1 else bi.end_klc.high, # 未完成笔没有结束价格 '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 not bi.is_sure], '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.is_sure], # 添加未完成线段列表 'uncompleted_seg_list': get_uncompleted_seg_list(analysis_result['seg_list'], client_tz), '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, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': zs.is_sure # 添加中枢是否完成的标志 } for zs in analysis_result['zs_list'] if zs.is_sure], # 添加主周期BI中枢列表(已完成) '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()) if getattr(zs.start_klc, 'end_time', None) else (zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat()) ), 'end_time': (zs.end_time if isinstance(zs.end_time, str) else zs.end_time.astimezone(client_tz).isoformat()) if getattr(zs, 'end_time', None) else None, 'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': bool(getattr(zs, 'is_sure', False)) } for zs in analysis_result.get('bi_zs_list', []) if getattr(zs, 'is_sure', False)], # 添加未完成中枢列表 '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, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': zs.is_sure # 未完成中枢的is_sure为False } for zs in analysis_result['zs_list'] if not zs.is_sure], # 添加未完成BI中枢列表 'uncompleted_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()) if getattr(zs.start_klc, 'end_time', None) else (zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat()) ), 'end_time': None, 'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': bool(getattr(zs, 'is_sure', False)) } for zs in analysis_result.get('bi_zs_list', []) if not getattr(zs, 'is_sure', False)], 'macd': macd_data, # 添加布林带数据 'bollinger': { 'upper': df['bb_upper'].tolist(), 'middle': df['bb_middle'].tolist(), 'lower': df['bb_lower'].tolist() }, 'element_bollinger': { 'upper': df['element_bb_upper'].tolist(), 'middle': df['element_bb_middle'].tolist(), 'lower': df['element_bb_lower'].tolist() }, # 添加ATR数据 'atr': df['atr'].tolist(), # 添加K线分型信息 'klc_fx_info': [{ 'time': format_time_safely(point['time'], client_tz), 'start_time': format_time_safely(point['start_time'], client_tz), 'end_time': format_time_safely(point['end_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']), # 是否为强分型 # 分型框(虚线矩形)用到的高低价 'high': float(point['high']) if point.get('high') is not None else None, 'low': float(point['low']) if point.get('low') is not None else None } for point in analysis_result['klc_fx_info']], # 添加ChanMACD分析数据 'chan_macd': serialize_chan_macd_data(analysis_result.get('chan_macd', {}), client_tz), # 添加多时间周期EMA52数据 'ema52_dict': analysis_result.get('ema52_dict', {}), # 直接输出KLC趋势标记(使用已有trend字段) 'klc_trend': klc_trend, # 添加主周期买卖点列表 # 注意:部分枚举在转为字符串时可能形如 "Chan_BSP_TYPE.BSP1(1)", # 这里进行健壮的解析,确保前端拿到的始终是 "BSP1" / "BUY" 这种简洁形式, # 以便与前端的 BSP_STYLE 键(如 "BSP1_BUY")正确匹配。 'bsp_list': [{ 'time': format_time_safely(bsp.end_time, client_tz), 'price': float(bsp.klc.low if 'BUY' in str(bsp.dir) else bsp.klc.high), # -- 规范化 type 名称,例如: # "Chan_BSP_TYPE.BSP1" -> "BSP1" # "Chan_BSP_TYPE.BSP1(1)" -> "BSP1" # "BSP1" -> "BSP1" 'type': ( lambda raw: ( (raw.split('.')[-1] if '.' in raw else raw).split('(')[0] ) )(str(bsp.type)), # -- 规范化 dir 名称,例如: # "Chan_BSP_DIR.BUY" -> "BUY" # "Chan_BSP_DIR.BUY(1)" -> "BUY" # "BUY" -> "BUY" 'dir': ( lambda raw: ( (raw.split('.')[-1] if '.' in raw else raw).split('(')[0] ) )(str(bsp.dir)), 'is_sure': bool(bsp.is_sure), 'sure_time': format_time_safely(bsp.sure_time, client_tz) if bsp.sure_time else None, 'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0 } for bsp in analysis_result.get('bsp_list', [])] }) # 如果有指定分形元素时间周期,获取小周期数据 if element_timeframe: # 获取小周期数据,使用与主周期相同的时间范围 element_df = get_kl_data(symbol, element_timeframe, start_time=start_time, end_time=end_time) if element_df is not None and len(element_df) > 0: # 添加小周期技术指标(包括布林带) element_df = add_indicators(element_df) # 对小周期数据进行缠论分析 element_analysis = analyze_chan(element_df, symbol, element_timeframe) # 计算小周期MACD数据 element_macd_data = calculate_macd(element_df) # 组装小周期 KLC 趋势(仅提取已有 trend,不做重算) try: element_klc_trend = [] for klc in element_analysis.get('klc_list', []): trend_val = getattr(klc, 'trend', None) t_obj = getattr(klc, 'end_time', None) or getattr(klc, 'start_time', None) if trend_val is None or t_obj is None: continue trend_name = str(trend_val) if '.' in trend_name: trend_name = trend_name.split('.')[-1] time_str = format_time_safely(t_obj, client_tz) if time_str: element_klc_trend.append({'time': time_str, 'trend': trend_name}) except Exception: element_klc_trend = [] # 添加小周期分析结果到返回数据 result['element_timeframe'] = element_timeframe result['element_macd'] = element_macd_data # 添加小周期MACD数据 # 添加小周期布林带数据 result['element_bollinger'] = { 'upper': element_df['bb_upper'].tolist(), 'middle': element_df['bb_middle'].tolist(), 'lower': element_df['bb_lower'].tolist() } result['element_element_bollinger'] = { 'upper': element_df['element_bb_upper'].tolist(), 'middle': element_df['element_bb_middle'].tolist(), 'lower': element_df['element_bb_lower'].tolist() } # 添加小周期ATR数据 result['element_atr'] = element_df['atr'].tolist() result['element_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 element_analysis['bi_list'] if bi.is_sure] # 添加次周期未完成笔列表 result['element_uncompleted_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_time, # 未完成笔没有结束时间 '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.low if convert_direction(bi.dir) == 1 else bi.end_klc.high, # 未完成笔没有结束价格 'direction': convert_direction(bi.dir), 'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0 } for bi in element_analysis['bi_list'] if not bi.is_sure] # 添加小周期K线数据 result['element_kline_data'] = clean_dataframe_for_json(element_df).to_dict('records') # 添加小周期KLC列表 result['element_klc_list'] = [{ 'date': klc.end_time if isinstance(klc.end_time, str) else klc.end_time.astimezone(client_tz).isoformat(), 'open': float(klc.open), 'high': float(klc.high), 'low': float(klc.low), 'close': float(klc.close), 'volume': float(klc.volume) if hasattr(klc, 'volume') else 0, 'direction': str(klc.dir).replace('Chan_KLINE_DIR.', ''), 'fx_type': str(klc.fx).replace('Chan_FX_TYPE.', ''), 'klc_fx_type': str(klc.klc_fx_type).replace('Chan_KLC_FX.', ''), 'trend': str(klc.trend).replace('Chan_PRICE_TREND.', '') } for klc in element_analysis['klc_list'] if hasattr(klc, 'end_time') and klc.end_time] result['element_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 element_analysis['seg_list'] if seg.is_sure] # 添加次周期未完成线段列表 result['element_uncompleted_seg_list'] = get_uncompleted_seg_list(element_analysis['seg_list'], client_tz) result['element_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, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': zs.is_sure # 添加中枢是否完成的标志 } for zs in element_analysis['zs_list'] if zs.end_klc] result['element_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, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': zs.is_sure # 未完成中枢的is_sure为False } for zs in element_analysis['zs_list'] if not zs.is_sure] # 添加次周期 BI 中枢(已完成/未完成) result['element_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()) if getattr(zs.start_klc, 'end_time', None) else (zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat()) ), 'end_time': (zs.end_time if isinstance(zs.end_time, str) else zs.end_time.astimezone(client_tz).isoformat()) if getattr(zs, 'end_time', None) else None, 'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': bool(getattr(zs, 'is_sure', False)) } for zs in element_analysis.get('bi_zs_list', []) if getattr(zs, 'is_sure', False)] result['element_uncompleted_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()) if getattr(zs.start_klc, 'end_time', None) else (zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat()) ), 'end_time': None, 'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': bool(getattr(zs, 'is_sure', False)) } for zs in element_analysis.get('bi_zs_list', []) if not getattr(zs, 'is_sure', False)] # 添加小周期分型信息 result['element_klc_fx_info'] = [{ 'time': format_time_safely(point['time'], client_tz), 'start_time': format_time_safely(point['start_time'], client_tz), 'end_time': format_time_safely(point['end_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']), # 是否为强分型 # 分型框(虚线矩形)用到的高低价 'high': float(point['high']) if point.get('high') is not None else None, 'low': float(point['low']) if point.get('low') is not None else None } for point in element_analysis['klc_fx_info']] # 添加次周期ChanMACD分析数据 result['element_chan_macd'] = serialize_chan_macd_data(element_analysis.get('chan_macd', {}), client_tz) # 添加小周期 KLC 趋势标记 result['element_klc_trend'] = element_klc_trend # 添加次周期买卖点列表 result['element_bsp_list'] = [{ 'time': format_time_safely(bsp.end_time, client_tz), 'price': float(bsp.klc.low if str(bsp.dir) == 'Chan_BSP_DIR.BUY' else bsp.klc.high), 'type': str(bsp.type).replace('Chan_BSP_TYPE.', ''), 'dir': str(bsp.dir).replace('Chan_BSP_DIR.', ''), 'is_sure': bool(bsp.is_sure), 'sure_time': format_time_safely(bsp.sure_time, client_tz) if bsp.sure_time else None, 'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0 } for bsp in element_analysis.get('bsp_list', [])] # 次次周期:仅当已指定次周期且次次周期有效时获取 if sub_sub_timeframe and is_smaller_or_equal_timeframe(sub_sub_timeframe, element_timeframe): sub_sub_df = get_kl_data(symbol, sub_sub_timeframe, start_time=start_time, end_time=end_time) if sub_sub_df is not None and len(sub_sub_df) > 0: sub_sub_df = add_indicators(sub_sub_df) sub_sub_analysis = analyze_chan(sub_sub_df, symbol, sub_sub_timeframe) result['sub_sub_timeframe'] = sub_sub_timeframe result['sub_sub_kline_data'] = clean_dataframe_for_json(sub_sub_df).to_dict('records') result['sub_sub_atr'] = sub_sub_df['atr'].tolist() result['sub_sub_macd'] = calculate_macd(sub_sub_df) result['sub_sub_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 sub_sub_analysis['bi_list'] if bi.is_sure] result['sub_sub_uncompleted_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_time, '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.low if convert_direction(bi.dir) == 1 else bi.end_klc.high, 'direction': convert_direction(bi.dir), 'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0 } for bi in sub_sub_analysis['bi_list'] if not bi.is_sure] # 次次周期 KLC 列表 result['sub_sub_klc_list'] = [{ 'date': klc.end_time if isinstance(klc.end_time, str) else klc.end_time.astimezone(client_tz).isoformat(), 'open': float(klc.open), 'high': float(klc.high), 'low': float(klc.low), 'close': float(klc.close), 'volume': float(klc.volume) if hasattr(klc, 'volume') else 0, 'direction': str(klc.dir).replace('Chan_KLINE_DIR.', ''), 'fx_type': str(klc.fx).replace('Chan_FX_TYPE.', ''), 'klc_fx_type': str(klc.klc_fx_type).replace('Chan_KLC_FX.', ''), 'trend': str(klc.trend).replace('Chan_PRICE_TREND.', '') } for klc in sub_sub_analysis.get('klc_list', []) if hasattr(klc, 'end_time') and klc.end_time] result['sub_sub_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 sub_sub_analysis['seg_list'] if seg.is_sure] result['sub_sub_uncompleted_seg_list'] = get_uncompleted_seg_list(sub_sub_analysis['seg_list'], client_tz) result['sub_sub_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, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': zs.is_sure } for zs in sub_sub_analysis['zs_list'] if zs.end_klc] result['sub_sub_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, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': zs.is_sure } for zs in sub_sub_analysis['zs_list'] if not zs.is_sure] result['sub_sub_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()) if getattr(zs.start_klc, 'end_time', None) else (zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat()) ), 'end_time': (zs.end_time if isinstance(zs.end_time, str) else zs.end_time.astimezone(client_tz).isoformat()) if getattr(zs, 'end_time', None) else None, 'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': bool(getattr(zs, 'is_sure', False)) } for zs in sub_sub_analysis.get('bi_zs_list', []) if getattr(zs, 'is_sure', False)] result['sub_sub_uncompleted_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()) if getattr(zs.start_klc, 'end_time', None) else (zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat()) ), 'end_time': None, 'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': bool(getattr(zs, 'is_sure', False)) } for zs in sub_sub_analysis.get('bi_zs_list', []) if not getattr(zs, 'is_sure', False)] result['sub_sub_klc_fx_info'] = [{ 'time': format_time_safely(point['time'], client_tz), 'start_time': format_time_safely(point['start_time'], client_tz), 'end_time': format_time_safely(point['end_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']), # 分型框(虚线矩形)用到的高低价 'high': float(point['high']) if point.get('high') is not None else None, 'low': float(point['low']) if point.get('low') is not None else None } for point in sub_sub_analysis['klc_fx_info']] result['sub_sub_bsp_list'] = [{ 'time': format_time_safely(bsp.end_time, client_tz), 'price': float(bsp.klc.low if str(bsp.dir) == 'Chan_BSP_DIR.BUY' else bsp.klc.high), 'type': str(bsp.type).replace('Chan_BSP_TYPE.', ''), 'dir': str(bsp.dir).replace('Chan_BSP_DIR.', ''), 'is_sure': bool(bsp.is_sure), 'sure_time': format_time_safely(bsp.sure_time, client_tz) if bsp.sure_time else None, 'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0 } for bsp in sub_sub_analysis.get('bsp_list', [])] result['sub_sub_chan_macd'] = serialize_chan_macd_data(sub_sub_analysis.get('chan_macd', {}), client_tz) try: sub_sub_klc_trend = [] for klc in sub_sub_analysis.get('klc_list', []): trend_val = getattr(klc, 'trend', None) t_obj = getattr(klc, 'end_time', None) or getattr(klc, 'start_time', None) if trend_val is None or t_obj is None: continue trend_name = str(trend_val) if '.' in trend_name: trend_name = trend_name.split('.')[-1] time_str = format_time_safely(t_obj, client_tz) if time_str: sub_sub_klc_trend.append({'time': time_str, 'trend': trend_name}) result['sub_sub_klc_trend'] = sub_sub_klc_trend except Exception: result['sub_sub_klc_trend'] = [] pass # 结构价值区分析(Structure Zone)—— 按需拉取:仅当 include_structure_zones 为真时执行多周期拉取(默认跳过以减轻负载) include_zones_param = request.args.get('include_structure_zones', '') include_structure_zones = str(include_zones_param).lower() in ('1', 'true', 'yes') if include_structure_zones: zone_timeframes_str = request.args.get('zone_timeframes', '') zone_kl_lines = int(request.args.get('zone_kl_lines', 1000)) try: zone_config = StructureZoneConfig(kl_lines_per_tf=zone_kl_lines) if zone_timeframes_str: zone_config.zone_timeframes = [t.strip() for t in zone_timeframes_str.split(',') if t.strip()] analyses = {} ema52_dict = {} latest_close = 0.0 now = time.time() def _fetch_single_tf_zone(tf_name): """单个时间周期的结构区数据拉取(线程安全)""" cache_key = f"{symbol}:{tf_name}:{zone_kl_lines}" cached = _zone_cache.get(cache_key) if cached and cached['expires'] > now: print(f" 结构区缓存命中: {tf_name}") return { 'tf_name': tf_name, 'analyses': cached['analyses'], 'ema52': cached['ema52'], 'close': cached.get('close', 0.0), 'cached': True, } try: tf_df = get_kl_data(symbol, tf_name, limit=zone_kl_lines) if tf_df is None or len(tf_df) == 0: return None tf_df = add_indicators(tf_df) tf_analysis = analyze_chan(tf_df, symbol, tf_name) zs_serialized = [{ '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()) if zs.start_klc else None, '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, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': zs.is_sure } for zs in tf_analysis.get('zs_list', []) if zs.is_sure] bi_zs_serialized = [{ '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()) if getattr(zs.start_klc, 'end_time', None) else (zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat())), 'end_time': (zs.end_time if isinstance(zs.end_time, str) else zs.end_time.astimezone(client_tz).isoformat()) if getattr(zs, 'end_time', None) else None, 'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': bool(getattr(zs, 'is_sure', False)) } for zs in tf_analysis.get('bi_zs_list', []) if getattr(zs, 'is_sure', False)] last_ema = tf_df['ema52'].iloc[-1] if 'ema52' in tf_df.columns else 0 ema_val = float(last_ema) if last_ema and last_ema > 0 else None last_close = float(tf_df['close'].iloc[-1]) tf_result = { 'tf_name': tf_name, 'analyses': {'zs_list': zs_serialized, 'bi_zs_list': bi_zs_serialized}, 'ema52': ema_val, 'close': last_close, 'cached': False, } # 写入缓存 _zone_cache[cache_key] = { 'analyses': tf_result['analyses'], 'ema52': ema_val, 'close': last_close, 'expires': now + _zone_cache_ttl(tf_name), } print(f" 结构区数据: {tf_name} -> zs={len(zs_serialized)}, bi_zs={len(bi_zs_serialized)}, ema52={ema_val}") return tf_result except Exception as e: print(f" 结构区 {tf_name} 拉取失败: {e}") return None with ThreadPoolExecutor(max_workers=len(zone_config.zone_timeframes)) as executor: futures = {executor.submit(_fetch_single_tf_zone, tf): tf for tf in zone_config.zone_timeframes} for future in as_completed(futures): tf_result = future.result() if tf_result is None: continue tf_name = tf_result['tf_name'] analyses[tf_name] = tf_result['analyses'] ema52_dict[tf_name] = tf_result['ema52'] if tf_result['close'] and (not latest_close or latest_close == 0.0): latest_close = tf_result['close'] structure_zones = analyze_structure_zones_from_serialized( analyses, ema52_dict, latest_close, config=zone_config ) result['structure_zones'] = [{ 'id': z.id, 'lower': z.lower, 'upper': z.upper, 'center': z.center, 'width_pct': z.width_pct, 'zone_type': z.zone_type, 'timeframes': z.timeframes, 'structure_types': z.structure_types, 'boundary_types': z.boundary_types, 'overlap_count': z.overlap_count, 'touch_count': z.touch_count, 'recency_score': z.recency_score, 'ema52_distance_pct': z.ema52_distance_pct, 'ema52_aligned': z.ema52_aligned, 'strength_score': z.strength_score, 'confidence': z.confidence, 'first_seen': z.first_seen, 'last_seen': z.last_seen, 'metadata': z.metadata, } for z in structure_zones] except Exception as e: print(f"StructureZone 分析出错: {e}") import traceback traceback.print_exc() result['structure_zones'] = [] else: result['structure_zones'] = [] # 威科夫分析 —— 按需:include_wyckoff=1 include_wyckoff_param = request.args.get('include_wyckoff', '') include_wyckoff = str(include_wyckoff_param).lower() in ('1', 'true', 'yes') if include_wyckoff: try: from chanlun.analysis.wyckoff import analyze_wyckoff wyckoff_lookback = int(request.args.get('wyckoff_lookback', 120)) wyckoff_bins = int(request.args.get('wyckoff_vp_bins', 50)) result['wyckoff'] = analyze_wyckoff( df, lookback=max(40, min(wyckoff_lookback, 500)), vp_bins=max(10, min(wyckoff_bins, 100)), ) except Exception as e: print(f"Wyckoff 分析出错: {e}") import traceback traceback.print_exc() result['wyckoff'] = { 'trading_range': None, 'bias': 'unknown', 'phases': [], 'events': [], 'volume_profile': {'bins': [], 'poc': None, 'vah': None, 'val': None, 'bin_count': 0}, 'volume_confirm': {'avg_volume': 0.0, 'event_checks': {}}, 'error': str(e), } return jsonify(result)