""" chan_integration.py — 缠论引擎集成:检测 BSP 信号并写入 signal_features。 复用 bsp_monitor/engine.py 的 ChanEngine 管线,对历史日线数据批量跑缠论, 提取 B1/B2/B3/S1/S2/S3 信号,通过 SignalTracker 记录到 signal_features。 """ import sys import os from datetime import date as Date, timedelta from typing import List, Optional import logging # 确保 Chan 引擎在路径上(与 bsp_monitor/engine.py 相同的路径设置) _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_TYPE, Chan_BSP_DIR from ChanBSP import ChanBSP logger = logging.getLogger(__name__) class ChanSignalDetector: """ 对历史日线数据运行缠论管线,提取所有 BSP 信号。 Usage: detector = ChanSignalDetector() signals = detector.detect_from_db("2026-01-01", "2026-06-24") # → [{"date": Date, "signal_type": "B3", "entry_price": 96500, ...}, ...] """ def __init__(self): from TF_DF import TF_DF as _TF_DF_Class self._TF_DF_Class = _TF_DF_Class def detect_from_db(self, start_date: str, end_date: str) -> list[dict]: """从数据库加载日线数据,跑缠论管线,提取信号。""" from database import get_connection conn = get_connection() df = pd.read_sql_query( "SELECT date, open, high, low, close, volume " "FROM ohlcv_daily WHERE symbol='BTC/USDT:USDT' " "AND date BETWEEN ? AND ? ORDER BY date", conn, params=(start_date, end_date) ) conn.close() if df.empty or len(df) < 50: logger.warning(f"日线数据不足: {len(df)} 根") return [] return self.detect_from_df(df) def detect_from_df(self, df: pd.DataFrame) -> list[dict]: """从 DataFrame 运行缠论管线,提取 BSP 信号。""" # 需要 datetime 列才能跑 TF_DF df = df.copy() df["timestamp"] = pd.to_datetime(df["date"]) df["date"] = df["timestamp"] try: engine = self._build_engine(df) except Exception as e: logger.error(f"缠论管线失败: {e}") return [] return self._extract_signals(engine) def _build_engine(self, df: pd.DataFrame): """构建缠论管线(对齐 bsp_monitor/engine.py 的 ChanEngine)。""" from TF_DF import TF_DF as _TF_DF_Class if df.empty or len(df) < 50: raise ValueError(f"数据不足: {len(df)} 根 K 线") if "date" not in df.columns and "timestamp" in df.columns: df["date"] = df["timestamp"] # 使用 __new__ 避免触发 TF_DF.__init__ engine = type('ChanEngine', (), {})() # 简单容器 tf = _TF_DF_Class.__new__(_TF_DF_Class) df_with_indicators = tf.add_indicators(df.copy()) engine.klu_list = tf.get_klu_list(df_with_indicators) engine.klc_list = tf.get_klc_list(engine.klu_list) engine.bi_list = tf.cal_bi_list(engine.klc_list) engine.seg_list = tf.get_seg_list(engine.bi_list) engine.bi_zs_list = tf.cal_bi_zs(engine.seg_list) engine.bsp_list = tf.find_all_bsp(engine.bi_list, engine.bi_zs_list) return engine def _extract_signals(self, engine) -> list[dict]: """从 ChanEngine 输出中提取所有 BSP 信号。""" signals = [] for bsp in engine.bsp_list: if bsp.type == Chan_BSP_TYPE.NONE: continue if bsp.klc is None: continue signal_type = self._bsp_type_str(bsp.type) entry_price = bsp.klc.close signal_date = self._klc_date(bsp.klc) if signal_date is None: continue # 信号质量:根据分型强度判断 strength = self._calc_strength(bsp) grade = "A" if strength >= 70 else "B" if strength >= 50 else "C" signals.append({ "date": signal_date, "signal_type": signal_type, "entry_price": float(entry_price), "signal_grade": grade, "signal_strength": float(strength), }) details = ", ".join(f"{s['signal_type']}({s['date']})" for s in signals) logger.info(f"检测到 {len(signals)} 个信号: {details}") return signals def populate_signal_features(self, start_date: str = "2024-01-01", end_date: Optional[str] = None) -> int: """ 完整流程:检测信号 → 计算市场状态 → 写入 signal_features。 Returns: 写入的信号数量。 """ if end_date is None: end_date = Date.today().isoformat() logger.info(f"开始信号检测: {start_date} → {end_date}") # Step 1: 检测缠论信号 signals = self.detect_from_db(start_date, end_date) if not signals: logger.warning("未检测到任何 BSP 信号") return 0 # Step 2: 去重 — 跳过已存在的信号 from database import get_connection conn = get_connection() existing = set() for row in conn.execute( "SELECT date, signal_type FROM signal_features" ).fetchall(): existing.add((row[0], row[1])) conn.close() new_signals = [s for s in signals if (str(s["date"]), s["signal_type"]) not in existing] if not new_signals: logger.info("所有信号已存在,跳过") return 0 # Step 3: 写入 signal_features from expectancy.tracker import SignalTracker tracker = SignalTracker() count = tracker.backfill_signals(new_signals) logger.info(f"信号入库完成: {count}/{len(signals)}") return count @staticmethod def _bsp_type_str(t: Chan_BSP_TYPE) -> str: mapping = { 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 mapping.get(t, "UNKNOWN") @staticmethod def _klc_date(klc) -> Optional[Date]: """从 KLC 提取信号确认日期。""" end_time = getattr(klc, "end_time", None) if end_time is None: start_time = getattr(klc, "start_time", None) if start_time is None: return None end_time = start_time if hasattr(end_time, "date"): return end_time.date() if isinstance(end_time, str): return Date.fromisoformat(end_time[:10]) return None @staticmethod def _calc_strength(bsp: ChanBSP) -> float: """根据 BSP 特征计算信号强度 0-100。""" score = 50.0 klc = bsp.klc if klc is None: return score # 分型强度 from ChanEnum import Chan_KLC_FX fx = getattr(klc, "klc_fx_type", None) if fx is not None: strong_fxs = {Chan_KLC_FX.TOP2, Chan_KLC_FX.TOP3, Chan_KLC_FX.BOTTOM2, Chan_KLC_FX.BOTTOM3} medium_fxs = {Chan_KLC_FX.TOP1, Chan_KLC_FX.BOTTOM1, Chan_KLC_FX.TOP4, Chan_KLC_FX.BOTTOM4} if fx in strong_fxs: score += 25 elif fx in medium_fxs: score += 10 # BSP 类型 if bsp.type in (Chan_BSP_TYPE.B1, Chan_BSP_TYPE.S1): score += 10 # 一类买卖点: 背驰确认, 额外加分 # 笔特征 bi = getattr(bsp, "bi", None) if bi and hasattr(bi, "height") and hasattr(bi, "width"): if bi.width > 3 and abs(bi.height) > 100: score += 10 return min(score, 100.0)