Files
Chan/ChanMacro/chan_integration.py
T
jackyu66git 3c72aa1310 chan_integration: auto-detect BSP signals from daily+4h Chan pipeline
- 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
2026-06-24 19:19:11 +08:00

209 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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)