Files
Chan/web/api/analyze.py
T
jackyu66gitandCursor 7f393b93ed refactor: 精简仓库为 chanlun 核心与 web 分析,移除威科夫与遗留模块
删除根目录旧 Chan 模块、策略、配置、文档及 wyckoff 相关代码;更新缠论 pipeline 与笔中枢计算;补充 research 研究与 web 测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 01:05:12 +08:00

739 lines
36 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""分析 API。"""
from flask import Blueprint, jsonify, request
from services.runtime import * # noqa: F403
from services import runtime as R
# import * 不会带出下划线私有名;结构区缓存需显式导入
from services.runtime.state import _zone_cache
from services.runtime.timeframes import _zone_cache_ttl
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'] = []
return jsonify(result)
def _serialize_kl_tail(df, limit: int):
"""只序列化最近 limit 根,供自动刷新增量合并。"""
if df is None or getattr(df, "empty", True):
return []
tail = df.tail(limit)
clean = clean_dataframe_for_json(tail)
records = clean.to_dict("records")
for row in records:
d = row.get("date")
if hasattr(d, "isoformat"):
try:
row["date"] = d.isoformat()
except Exception:
row["date"] = str(d)
# timestamp 统一成 int ms,便于前端按 key 合并
ts = row.get("timestamp")
if ts is not None:
try:
row["timestamp"] = int(ts)
except (TypeError, ValueError):
pass
elif hasattr(d, "timestamp"):
try:
row["timestamp"] = int(d.timestamp() * 1000)
except Exception:
pass
return records
@bp.route("/api/klines/recent")
def klines_recent():
"""轻量拉取最近 N 根 K 线(不做缠论/威科夫),供主站自动刷新增量。"""
symbol = (request.args.get("symbol") or "").strip()
if not symbol:
return jsonify({"error": "交易对不能为空"}), 400
timeframe = request.args.get("timeframe", "5m")
try:
limit = int(request.args.get("limit", 2))
except (TypeError, ValueError):
limit = 2
limit = max(1, min(limit, 20))
element_timeframe = request.args.get("element_timeframe") or None
sub_sub_timeframe = request.args.get("sub_sub_timeframe") or None
# 只取尾部:不传 start/end,避免全量窗口回拉
df = get_kl_data(symbol, timeframe, limit=limit)
if df is None:
return jsonify({"error": "获取数据失败"}), 502
if len(df) == 0:
return jsonify({"error": "没有数据"}), 404
result = {
"partial": True,
"symbol": symbol,
"timeframe": timeframe,
"limit": limit,
"kline_data": _serialize_kl_tail(df, limit),
}
if element_timeframe:
edf = get_kl_data(symbol, element_timeframe, limit=limit)
result["element_timeframe"] = element_timeframe
result["element_kline_data"] = _serialize_kl_tail(edf, limit) if edf is not None else []
if sub_sub_timeframe:
sdf = get_kl_data(symbol, sub_sub_timeframe, limit=limit)
result["sub_sub_timeframe"] = sub_sub_timeframe
result["sub_sub_kline_data"] = _serialize_kl_tail(sdf, limit) if sdf is not None else []
return jsonify(result)