""" engine.py - 缠论管线封装:DataFrame → KLU → KLC → BI → SEG → ZS → BSP。 复用 ~/Project/Chan/ 下的 TF_DF 模块,管线步骤对齐 TF_DF.get_bsp_state()。 """ import sys import os from typing import List, Optional _PARENT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if _PARENT not in sys.path: sys.path.insert(0, _PARENT) import pandas as pd from ChanEnum import ( Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_ZS_DIR, ) from ChanBSP import ChanBSP from ChanBI import ChanBI # 仅导入类,不触发 TF_DF.__init__ from TF_DF import TF_DF as _TF_DF_Class class ChanEngine: """缠论管线,对齐 TF_DF.get_bsp_state() 的调用顺序。""" def __init__(self, df: pd.DataFrame): if df.empty or len(df) < 50: raise ValueError("DataFrame 至少需要 50 根 K 线") if "date" not in df.columns and "timestamp" in df.columns: df["date"] = df["timestamp"] self.df = df self._tf = _TF_DF_Class.__new__(_TF_DF_Class) # 不调用 __init__ # Step 0: 添加 TA 指标 (MACD/EMA/BB/RSI) self._df_with_indicators = self._tf.add_indicators(df.copy()) # Step 1: KLU — get_klu_list → get_kl_data → cal_kl_data self.klu_list = self._tf.get_klu_list(self._df_with_indicators) # Step 2: KLC — 内部已含 ChanMACD.cal_macd_state() + cal_trend() self.klc_list = self._tf.get_klc_list(self.klu_list) # Step 3: BI (stroke) self.bi_list = self._tf.cal_bi_list(self.klc_list) # Step 4: SEG (segment) self.seg_list = self._tf.get_seg_list(self.bi_list) # Step 5: ZS — cal_bi_zs(seg_list) 对齐 get_bsp_state(从线段计算笔中枢) self.bi_zs_list: List = self._tf.cal_bi_zs(self.seg_list) # Step 6: BSP (buy/sell points) self.bsp_list: List[ChanBSP] = self._tf.find_all_bsp( self.bi_list, self.bi_zs_list ) def get_second_last_bi(self) -> Optional[ChanBI]: """获取倒数第二笔(最新确认的笔)。""" confirmed = [b for b in self.bi_list if b.is_sure] if len(confirmed) >= 2: return confirmed[-2] elif len(confirmed) == 1: return confirmed[-1] return None def get_bsp_for_bi(self, bi: ChanBI) -> Optional[ChanBSP]: """检查某个 Bi 的 end_klc 是否是买卖点。""" if bi is None or not bi.is_sure: return None klc = bi.end_klc if klc is None: return None if klc.bsp and klc.bsp_type != Chan_BSP_TYPE.NONE: for bsp in self.bsp_list: if bsp.klc is klc: return bsp return None # ── 格式化 ── @staticmethod def _bsp_type_name(t: Chan_BSP_TYPE) -> str: import ChanEnum names = { Chan_BSP_TYPE.B1: "一类买点(B1)", Chan_BSP_TYPE.B2: "二类买点(B2)", Chan_BSP_TYPE.B3: "三类买点(B3)", Chan_BSP_TYPE.S1: "一类卖点(S1)", Chan_BSP_TYPE.S2: "二类卖点(S2)", Chan_BSP_TYPE.S3: "三类卖点(S3)", } return names.get(t, str(t)) @staticmethod def _bi_dir_name(d) -> str: return "⬆️ 向上" if d == Chan_BI_DIR.UP else "⬇️ 向下" @staticmethod def _fx_strength_name(klc_fx_type) -> str: import ChanEnum names = { Chan_KLC_FX.TOP0: "TOP0(弱)", Chan_KLC_FX.TOP1: "TOP1(标准)", Chan_KLC_FX.TOP2: "TOP2(强)", Chan_KLC_FX.TOP3: "TOP3(二类)", Chan_KLC_FX.TOP4: "TOP4(BB上轨)", Chan_KLC_FX.TOP5: "TOP5", Chan_KLC_FX.TOP6: "TOP6(高位空)", Chan_KLC_FX.TOP7: "TOP7(背驰)", Chan_KLC_FX.TOP8: "TOP8(信号线)", Chan_KLC_FX.BOTTOM0: "BOTTOM0(弱)", Chan_KLC_FX.BOTTOM1: "BOTTOM1(标准)", Chan_KLC_FX.BOTTOM2: "BOTTOM2(强)", Chan_KLC_FX.BOTTOM3: "BOTTOM3(二类)", Chan_KLC_FX.BOTTOM4: "BOTTOM4(BB下轨)", Chan_KLC_FX.BOTTOM5: "BOTTOM5(零轴下)", Chan_KLC_FX.BOTTOM6: "BOTTOM6(高位空)", Chan_KLC_FX.BOTTOM7: "BOTTOM7(背驰)", Chan_KLC_FX.BOTTOM8: "BOTTOM8(信号线)", } return names.get(klc_fx_type, f"UNKNOWN({klc_fx_type})") @staticmethod def _utc_to_cst(time_str: str) -> str: """UTC 时间字符串 → 东八区 (UTC+8)。""" from datetime import datetime, timedelta, timezone dt = datetime.fromisoformat(str(time_str)) if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) cst = dt.astimezone(timezone(timedelta(hours=8))) return cst.strftime("%Y-%m-%d %H:%M:%S CST") def format_bsp_detail(self, bsp: ChanBSP) -> str: bi = bsp.bi klc = bsp.klc bsp_type = bsp.type bsp_dir = bsp.dir emoji = "🟢" if bsp_dir == Chan_BSP_DIR.BUY else "🔴" dir_label = "买点" if bsp_dir == Chan_BSP_DIR.BUY else "卖点" lines = [ f"{emoji} [{dir_label}] {self._bsp_type_name(bsp_type)} — BTC/USDT 1m", "", f"⏰ 确认: {self._utc_to_cst(klc.end_time)}", f"💰 价格: {klc.close:.2f}", f"📐 笔方向: {self._bi_dir_name(bi.dir)}", f"📏 笔高度: ${bi.height:.2f} 宽度: {bi.width}K 斜率: {bi.slop:.2f}", f"🔩 分型强度: {self._fx_strength_name(klc.klc_fx_type)}", ] if bsp.zs: zs = bsp.zs zs_dir = "UP" if hasattr(zs, 'dir') and hasattr(Chan_ZS_DIR, 'UP') and zs.dir == Chan_ZS_DIR.UP else "DOWN" lines.append(f"🏠 中枢: {zs.zd:.2f} – {zs.zg:.2f} ({zs_dir}, #{getattr(zs, 'index', 0) + 1})") if bsp.type in (Chan_BSP_TYPE.B1, Chan_BSP_TYPE.S1): lines.append("📊 MACD背驰: 有 (离开段能量 < 进入段)") if hasattr(klc, 'ema_status') and klc.ema_status: ema52 = klc.ema_status.get('ema52', {}) if ema52: pos = str(ema52.get('pos', '?')) lines.append(f"📈 EMA52: {pos} (值: {klc.ema52:.2f})") lines.append(f"📋 KLC状态: {klc.klc_state}") if bi.pre: prev = bi.pre lines.extend([ "────", f"⬅️ 前一笔: {self._bi_dir_name(prev.dir)} " f"高度: ${prev.height:.2f} 宽度: {prev.width}K", ]) return "\n".join(lines)