添加 bsp_monitor: BTC/USDT 1m 缠论买卖点实时监控
- 每整分钟拉取 Binance 永续合约 1m K 线 - 运行完整缠论管线检测买卖点 (BSP) - 新 BSP 推送到 Telegram - fix: fetcher 用 limit=1000 替代固定 since,避免 API 500 根限制截断新数据
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
"""
|
||||
engine.py - 缠论管线封装:DataFrame → KLC → BI → ZS → BSP。
|
||||
|
||||
复用 ~/Project/Chan/ 下的 TF_DF 模块。
|
||||
注意:TF_DF.__init__ 有 bug(get_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)
|
||||
Reference in New Issue
Block a user