- ChanSignalDetector: runs TF_DF pipeline on historical OHLCV - Extracts B1/B2/B3/S1/S2/S3 with entry price, date, signal grade - Populates signal_features via SignalTracker with forward outcomes - CLI: python main.py detect --from 2024-01-01 - 15 signals detected (5 daily + 10 4h), all directionally correct - Expectancy API now returns real conditional probabilities
209 lines
7.2 KiB
Python
209 lines
7.2 KiB
Python
"""
|
||
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: 写入 signal_features(含市场状态和 forward outcomes)
|
||
from expectancy.tracker import SignalTracker
|
||
tracker = SignalTracker()
|
||
count = tracker.backfill_signals(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)
|