三周期分开关控制,只画数字不画图标;线段面积比沿用同向笔面积口径。 Co-authored-by: Cursor <cursoragent@cursor.com>
339 lines
12 KiB
Python
339 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import pandas as pd
|
|
|
|
from chanlun.core.ChanEnum import Chan_BI_DIR, Chan_SEG_DIR, Chan_MACDSEG_DIR, Chan_MACDHISTSET_DIR
|
|
|
|
# 辅助函数,转换缠论方向枚举为整数
|
|
def convert_direction(direction):
|
|
"""转换方向枚举为数字"""
|
|
if direction == Chan_BI_DIR.UP or direction == Chan_SEG_DIR.UP:
|
|
return 1
|
|
elif direction == Chan_BI_DIR.DOWN or direction == Chan_SEG_DIR.DOWN:
|
|
return -1
|
|
else:
|
|
return 0
|
|
|
|
def format_time_safely(time_obj, client_tz):
|
|
"""安全地格式化时间对象,处理字符串和datetime两种情况"""
|
|
if time_obj is None:
|
|
return None
|
|
|
|
if isinstance(time_obj, str):
|
|
# 尝试将字符串解析为datetime
|
|
try:
|
|
from dateutil import parser
|
|
time_obj = parser.parse(time_obj)
|
|
return time_obj.astimezone(client_tz).isoformat()
|
|
except:
|
|
return time_obj
|
|
else:
|
|
# 已经是datetime对象
|
|
return time_obj.astimezone(client_tz).isoformat()
|
|
|
|
def serialize_fast_bsp_list(fast_bsp_list, client_tz):
|
|
"""序列化第四类买卖点(ChanFastBSP)。
|
|
|
|
htf_agree(大级别分型同向)与 ladder_ok(中枢顺向推进)分别输出,
|
|
由前端决定要显示全部还是只显示两者都满足的。
|
|
"""
|
|
out = []
|
|
for bsp in fast_bsp_list or []:
|
|
try:
|
|
out.append({
|
|
'time': format_time_safely(bsp.time, client_tz),
|
|
'price': float(bsp.price),
|
|
'type': str(bsp.type).split('.')[-1].split('(')[0],
|
|
'dir': str(bsp.dir).split('.')[-1].split('(')[0],
|
|
'is_sure': True,
|
|
'lag': int(bsp.lag),
|
|
'depth': float(bsp.depth),
|
|
'zg': bsp.zg,
|
|
'zd': bsp.zd,
|
|
'occ': int(bsp.occ),
|
|
'htf_agree': bsp.htf_agree,
|
|
'ladder_ok': bsp.ladder_ok,
|
|
'bo_time': format_time_safely(bsp.bo_time, client_tz) if bsp.bo_time else None,
|
|
'pb_time': format_time_safely(bsp.pb_time, client_tz) if bsp.pb_time else None,
|
|
})
|
|
except Exception as e:
|
|
print(f"序列化fast_bsp出错: {e}")
|
|
continue
|
|
return out
|
|
|
|
def serialize_chan_macd_data(chan_macd_data, client_tz):
|
|
"""序列化ChanMACD数据为JSON可序列化格式"""
|
|
serialized_data = {
|
|
'seg_list': [],
|
|
'unittf_list': [],
|
|
'histset_list': [],
|
|
# 状态标记数据
|
|
'high_position_list': [],
|
|
'high_empty_list': [],
|
|
'low_position_list': [],
|
|
'low_empty_list': [],
|
|
'return_zero_list': [],
|
|
'cross0_up_list': [],
|
|
'cross0_down_list': [],
|
|
# 新增:输出KLU的继续背驰/分离背驰标志
|
|
'klu_list': []
|
|
}
|
|
|
|
# 序列化seg_list
|
|
for seg in chan_macd_data.get('seg_list', []):
|
|
try:
|
|
seg_data = {
|
|
'start_time': format_time_safely(seg.start_time, client_tz),
|
|
'end_time': format_time_safely(seg.end_time, client_tz) if seg.end_time else None,
|
|
'seg_dir': 'ABOVE' if seg.seg_dir == Chan_MACDSEG_DIR.ABOVE else 'UNDER',
|
|
'klu_count': len(seg.klu_list) if hasattr(seg, 'klu_list') else 0,
|
|
'unittf_count': len(seg.unittf_list) if hasattr(seg, 'unittf_list') else 0,
|
|
'histset_count': len(seg.hist_set) if hasattr(seg, 'hist_set') else 0
|
|
}
|
|
serialized_data['seg_list'].append(seg_data)
|
|
except Exception as e:
|
|
print(f"序列化seg出错: {e}")
|
|
continue
|
|
|
|
# 序列化unittf_list(兼容新结构与枚举类型)
|
|
for unittf in chan_macd_data.get('unittf_list', []):
|
|
try:
|
|
dir_value = getattr(unittf, 'unittf_dir', None)
|
|
dir_name = getattr(dir_value, 'name', dir_value if isinstance(dir_value, str) else None)
|
|
if dir_name == 'ABOVE':
|
|
dir_num = 1
|
|
elif dir_name == 'UNDER':
|
|
dir_num = -1
|
|
else:
|
|
dir_num = 0
|
|
start_t = getattr(unittf, 'start_type', None)
|
|
start_type = getattr(start_t, 'name', start_t)
|
|
end_t = getattr(unittf, 'end_type', None)
|
|
end_type = getattr(end_t, 'name', end_t)
|
|
peak_abs = getattr(unittf, 'peak_abs', None)
|
|
if peak_abs is None:
|
|
peak_abs = getattr(unittf, 'peak_hist', None)
|
|
length = getattr(unittf, 'length', None)
|
|
if length is None:
|
|
length = len(unittf.klu_list) if hasattr(unittf, 'klu_list') else None
|
|
|
|
unittf_data = {
|
|
'start_time': format_time_safely(getattr(unittf, 'start_time', None), client_tz),
|
|
'end_time': format_time_safely(getattr(unittf, 'end_time', None), client_tz) if getattr(unittf, 'end_time', None) else None,
|
|
'dir': dir_num,
|
|
'start_type': start_type, # e.g. 'START' | 'CROSS0' | 'NEAR0_UP' | 'NEAR0_DOWN'
|
|
'end_type': end_type,
|
|
'invalid': getattr(unittf, 'invalid', False),
|
|
'peak_abs': peak_abs,
|
|
'length': length,
|
|
'klu_count': len(unittf.klu_list) if hasattr(unittf, 'klu_list') else 0,
|
|
'histset_count': len(unittf.histset_list) if hasattr(unittf, 'histset_list') else 0
|
|
}
|
|
serialized_data['unittf_list'].append(unittf_data)
|
|
except Exception as e:
|
|
print(f"序列化unittf出错: {e}")
|
|
continue
|
|
|
|
# 序列化histset_list
|
|
for histset in chan_macd_data.get('histset_list', []):
|
|
try:
|
|
histset_data = {
|
|
'start_time': format_time_safely(getattr(histset, 'start_time', None), client_tz),
|
|
'end_time': format_time_safely(getattr(histset, 'end_time', None), client_tz),
|
|
'histset_dir': 'ABOVE' if histset.histset_dir == Chan_MACDHISTSET_DIR.ABOVE else 'UNDER',
|
|
'klu_count': len(histset.klu_list) if hasattr(histset, 'klu_list') else 0
|
|
}
|
|
serialized_data['histset_list'].append(histset_data)
|
|
except Exception as e:
|
|
print(f"序列化histset出错: {e}")
|
|
continue
|
|
|
|
# 序列化状态标记数据
|
|
# 序列化高位列表
|
|
for high_pos in chan_macd_data.get('high_position_list', []):
|
|
try:
|
|
high_pos_data = {
|
|
'time': format_time_safely(high_pos['time'], client_tz),
|
|
'end_time': format_time_safely(high_pos.get('end_time'), client_tz) if high_pos.get('end_time') else None,
|
|
'type': high_pos.get('type', 'start'),
|
|
'macd': high_pos.get('macd'),
|
|
'signal': high_pos.get('signal'),
|
|
'macdhist': high_pos.get('macdhist'),
|
|
'end_macd': high_pos.get('end_macd'),
|
|
'end_signal': high_pos.get('end_signal'),
|
|
'end_macdhist': high_pos.get('end_macdhist')
|
|
}
|
|
serialized_data['high_position_list'].append(high_pos_data)
|
|
except Exception as e:
|
|
print(f"序列化high_position出错: {e}")
|
|
continue
|
|
|
|
# 序列化高位空列表
|
|
for high_empty in chan_macd_data.get('high_empty_list', []):
|
|
try:
|
|
high_empty_data = {
|
|
'time': format_time_safely(high_empty['time'], client_tz),
|
|
'end_time': format_time_safely(high_empty.get('end_time'), client_tz) if high_empty.get('end_time') else None,
|
|
'type': high_empty.get('type', 'start'),
|
|
'macd': high_empty.get('macd'),
|
|
'signal': high_empty.get('signal'),
|
|
'macdhist': high_empty.get('macdhist'),
|
|
'end_macd': high_empty.get('end_macd'),
|
|
'end_signal': high_empty.get('end_signal'),
|
|
'end_macdhist': high_empty.get('end_macdhist')
|
|
}
|
|
serialized_data['high_empty_list'].append(high_empty_data)
|
|
except Exception as e:
|
|
print(f"序列化high_empty出错: {e}")
|
|
continue
|
|
|
|
# 序列化低位与低位空
|
|
for low_pos in chan_macd_data.get('low_position_list', []):
|
|
try:
|
|
low_pos_data = {
|
|
'time': format_time_safely(low_pos['time'], client_tz),
|
|
'end_time': format_time_safely(low_pos.get('end_time'), client_tz) if low_pos.get('end_time') else None,
|
|
'type': low_pos.get('type', 'start'),
|
|
'macd': low_pos.get('macd'),
|
|
'signal': low_pos.get('signal'),
|
|
'macdhist': low_pos.get('macdhist'),
|
|
'end_macd': low_pos.get('end_macd'),
|
|
'end_signal': low_pos.get('end_signal'),
|
|
'end_macdhist': low_pos.get('end_macdhist')
|
|
}
|
|
serialized_data['low_position_list'].append(low_pos_data)
|
|
except Exception as e:
|
|
print(f"序列化low_position出错: {e}")
|
|
continue
|
|
|
|
for low_empty in chan_macd_data.get('low_empty_list', []):
|
|
try:
|
|
low_empty_data = {
|
|
'time': format_time_safely(low_empty['time'], client_tz),
|
|
'end_time': format_time_safely(low_empty.get('end_time'), client_tz) if low_empty.get('end_time') else None,
|
|
'type': low_empty.get('type', 'start'),
|
|
'macd': low_empty.get('macd'),
|
|
'signal': low_empty.get('signal'),
|
|
'macdhist': low_empty.get('macdhist'),
|
|
'end_macd': low_empty.get('end_macd'),
|
|
'end_signal': low_empty.get('end_signal'),
|
|
'end_macdhist': low_empty.get('end_macdhist')
|
|
}
|
|
serialized_data['low_empty_list'].append(low_empty_data)
|
|
except Exception as e:
|
|
print(f"序列化low_empty出错: {e}")
|
|
continue
|
|
|
|
# 序列化归零轴列表
|
|
for return_zero in chan_macd_data.get('return_zero_list', []):
|
|
try:
|
|
return_zero_data = {
|
|
'time': format_time_safely(return_zero['time'], client_tz),
|
|
'end_time': format_time_safely(return_zero.get('end_time'), client_tz) if return_zero.get('end_time') else None,
|
|
'type': return_zero.get('type', 'start'),
|
|
'macd': return_zero.get('macd'),
|
|
'signal': return_zero.get('signal'),
|
|
'macdhist': return_zero.get('macdhist'),
|
|
'end_macd': return_zero.get('end_macd'),
|
|
'end_signal': return_zero.get('end_signal'),
|
|
'end_macdhist': return_zero.get('end_macdhist')
|
|
}
|
|
serialized_data['return_zero_list'].append(return_zero_data)
|
|
except Exception as e:
|
|
print(f"序列化return_zero出错: {e}")
|
|
continue
|
|
|
|
# 序列化穿越零轴列表
|
|
for cross0_up in chan_macd_data.get('cross0_up_list', []):
|
|
try:
|
|
cross0_up_data = {
|
|
'time': format_time_safely(cross0_up['time'], client_tz),
|
|
'type': cross0_up.get('type', 'start'),
|
|
'macd': cross0_up.get('macd'),
|
|
'signal': cross0_up.get('signal'),
|
|
'macdhist': cross0_up.get('macdhist')
|
|
}
|
|
serialized_data['cross0_up_list'].append(cross0_up_data)
|
|
except Exception as e:
|
|
print(f"序列化cross0_up出错: {e}")
|
|
continue
|
|
|
|
for cross0_down in chan_macd_data.get('cross0_down_list', []):
|
|
try:
|
|
cross0_down_data = {
|
|
'time': format_time_safely(cross0_down['time'], client_tz),
|
|
'type': cross0_down.get('type', 'start'),
|
|
'macd': cross0_down.get('macd'),
|
|
'signal': cross0_down.get('signal'),
|
|
'macdhist': cross0_down.get('macdhist')
|
|
}
|
|
serialized_data['cross0_down_list'].append(cross0_down_data)
|
|
except Exception as e:
|
|
print(f"序列化cross0_down出错: {e}")
|
|
continue
|
|
|
|
# 序列化 KLU 列表(仅导出需要的时间与背驰标志)
|
|
for klu in chan_macd_data.get('klu_list', []):
|
|
try:
|
|
serialized_data['klu_list'].append({
|
|
'time': format_time_safely(getattr(klu, 'time', None), client_tz),
|
|
'continue_div': bool(getattr(klu, 'continue_div', False)),
|
|
'separate_div': int(getattr(klu, 'separate_div', 0)) if getattr(klu, 'separate_div', 0) is not None else 0,
|
|
'near0_return': int(getattr(klu, 'near0_return', 0)) if getattr(klu, 'near0_return', 0) is not None else 0
|
|
})
|
|
except Exception as e:
|
|
print(f"序列化klu出错: {e}")
|
|
continue
|
|
|
|
return serialized_data
|
|
|
|
def clean_dataframe_for_json(df):
|
|
"""清理DataFrame数据用于JSON序列化"""
|
|
# 创建副本避免修改原始数据
|
|
clean_df = df.copy()
|
|
|
|
# 替换NaN值为None
|
|
clean_df = clean_df.where(pd.notnull(clean_df), None)
|
|
|
|
return clean_df
|
|
|
|
def get_uncompleted_seg_list(seg_list, client_tz):
|
|
"""获取未完成线段列表,正确处理倒数第二个和最后一个未完成线段"""
|
|
uncompleted_segs = [seg for seg in seg_list if not seg.is_sure]
|
|
|
|
if len(uncompleted_segs) == 0:
|
|
return []
|
|
|
|
result = []
|
|
|
|
for i, seg in enumerate(uncompleted_segs):
|
|
is_last = (i == len(uncompleted_segs) - 1) # 是否为最后一个未完成线段
|
|
|
|
seg_data = {
|
|
'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(),
|
|
'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,
|
|
'direction': convert_direction(seg.dir),
|
|
'macd_div': float(getattr(seg, 'macd_div', 0) or 0),
|
|
'macd_hist': float(getattr(seg, 'macd_hist', 0) or 0)
|
|
}
|
|
|
|
if is_last:
|
|
# 最后一个未完成线段:没有结束时间和价格
|
|
seg_data['end_time'] = None
|
|
seg_data['end_price'] = None
|
|
else:
|
|
# 倒数第二个及之前的未完成线段:使用实际的结束时间和价格
|
|
if seg.end_bi and seg.end_bi.end_klc:
|
|
seg_data['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()
|
|
seg_data['end_price'] = seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low
|
|
else:
|
|
# 如果没有结束笔,设为None
|
|
seg_data['end_time'] = None
|
|
seg_data['end_price'] = None
|
|
|
|
result.append(seg_data)
|
|
|
|
return result
|
|
|