Files
Chan/bsp_monitor/engine.py
T
jackyu66git f0ea6a6065 添加 bsp_monitor: BTC/USDT 1m 缠论买卖点实时监控
- 每整分钟拉取 Binance 永续合约 1m K 线
- 运行完整缠论管线检测买卖点 (BSP)
- 新 BSP 推送到 Telegram
- fix: fetcher 用 limit=1000 替代固定 since,避免 API 500 根限制截断新数据
2026-05-18 08:43:23 +08:00

182 lines
6.6 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.
"""
engine.py - 缠论管线封装:DataFrame → KLC → BI → ZS → BSP。
复用 ~/Project/Chan/ 下的 TF_DF 模块。
注意:TF_DF.__init__ 有 bugget_zs_list 不存在),这里手动调用各步骤。
"""
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
import talib.abstract as ta
from ChanEnum import (
Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_BI_DIR,
Chan_FX_TYPE, Chan_KLINE_DIR, Chan_SEG_DIR, Chan_ZS_DIR,
)
from ChanBSP import ChanBSP
from ChanBI import ChanBI
from ChanZS import ChanZS
# 仅导入类,不触发 TF_DF.__init__
from TF_DF import TF_DF as _TF_DF_Class
class ChanEngine:
"""手动执行缠论管线,绕过 TF_DF.__init__ 的 bug。"""
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 指标
self._df_with_indicators = self._tf.add_indicators(df.copy())
# Step 1: KLU (K-line unit)
self.klu_list = self._tf.cal_kl_data(self._df_with_indicators)
# Step 1.5: MACD state
from ChanMACD import ChanMACD
chanmacd = ChanMACD(self.klu_list)
self.klu_list = chanmacd.cal_macd_state()
# Step 2: KLC (combined K-line)
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 (bi-level center) — 供 find_all_bsp 使用
self.bi_zs_list: List = self._tf.cal_bi_zs_list(self.bi_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)} — <b>BTC/USDT 1m</b>",
"",
f"⏰ 确认: <code>{self._utc_to_cst(klc.end_time)}</code>",
f"💰 价格: <b>{klc.close:.2f}</b>",
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)