refactor: ECR-002 拆分 runtime 包并加深 analyze 契约(已审)
将 web/services/runtime.py 拆为 runtime/ 子模块并保持门面兼容;补齐 ESS 文档、门面/契约/TF_DF 测试与 CODE_REVIEW Approve。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
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_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, 'uinttf_dir', None)
|
||||
dir_name = getattr(dir_value, 'name', dir_value if isinstance(dir_value, str) else None)
|
||||
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_name, # 'ABOVE' | 'UNDER' | None
|
||||
'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)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user