@@ -0,0 +1,660 @@
""" 分析 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 ' ] = [ ]
return jsonify ( result )