Files
Chan/bsp_monitor/main.py
T
jackyu66gitandClaude Opus 4.7 b1cbdca707 ChanPivotMonitor: 实时中枢特征跟踪 + Telegram推送
- ChanPivotClassifier: 提取 calc_duration/contraction/shift 为 @staticmethod,新增 compute_features()
- ChanPivotMonitor: 实时追踪当前中枢,bi_count 增长时重新计算 shift/contraction/duration
- bsp_monitor/fetcher: 改用 data_provider HTTP API 替代直连 CCXT
- bsp_monitor/notify: 新增 send_telegram_message() 通用推送
- bsp_monitor/main: 集成 ChanPivotMonitor,有新笔或 BSP 时推送到 Telegram

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:35:09 +08:00

206 lines
7.5 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.
#!/usr/bin/env python3
"""
main.py - 缠论买卖点监控主程序。
每整分钟:
1. 从 Binance 拉取最新 1m K 线
2. 跑完整缠论管线
3. 检测新出现的买卖点(BSP)
4. 推送到 Telegram(同一 BSP 只推一次)
"""
import asyncio
import logging
import sys
import os
import time
from datetime import datetime, timezone, timedelta
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from fetcher import fetch_ohlcv
from engine import ChanEngine
from notify import send_bsp_alert, send_telegram_message, BOT_TOKEN, CHAT_ID
_PARENT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _PARENT not in sys.path:
sys.path.insert(0, _PARENT)
from ChanPivotMonitor import ChanPivotMonitor
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger("bsp_monitor")
def _bsp_stable_key(bsp) -> str:
"""生成 BSP 的稳定唯一键(不依赖笔边界的微小变化)。"""
return f"{bsp.type}_{bsp.klc.end_time}"
class BSPMonitor:
def __init__(self):
self._first_run = True
self._last_df_ts = None
self._known_bsp_keys: set = set() # 已见过的 BSP 键(含已推送和历史的)
self.pivot_monitor = ChanPivotMonitor(window_size=10)
async def tick(self):
"""单次 tick。"""
tick_start = time.monotonic()
logger.info("── tick 开始 ──")
# 1. 拉取 K 线
try:
df = fetch_ohlcv()
except Exception as e:
logger.error(f"拉取 K 线失败: {e}")
return
if df.empty:
logger.warning("DataFrame 为空,跳过")
return
latest_ts = df.iloc[-1]["timestamp"]
if self._last_df_ts and latest_ts <= self._last_df_ts:
logger.info("无新 K 线,跳过")
return
self._last_df_ts = latest_ts
# 2. 运行缠论管线
try:
engine = ChanEngine(df)
except Exception as e:
logger.error(f"缠论计算失败: {e}", exc_info=True)
return
# 2.5 更新中枢特征监控 + 推送
pivot_state = self.pivot_monitor.update(engine.bi_zs_list)
if pivot_state:
logger.info(
f"中枢特征更新: bi_count={pivot_state['bi_count']} "
f"is_sure={pivot_state['is_sure']} "
f"contraction={pivot_state['contraction']:.4f} "
f"shift_norm={pivot_state['shift_norm']:+.4f} "
f"duration_norm={pivot_state['duration_norm']:.4f}"
)
# Telegram 推送
if pivot_state["is_sure"]:
phase = "✅ 已确认"
elif pivot_state["bi_count"] > 3:
phase = "🔄 延伸中"
else:
phase = "🆕 刚形成"
zs_dir = pivot_state["zs_dir"]
dir_label = "⬆️ 向上" if "UP" in zs_dir else "⬇️ 向下"
# 白话解释
c = pivot_state["contraction"]
if c < 0.85:
contraction_note = "收敛(振幅缩小,可能快出方向)"
elif c > 1.15:
contraction_note = "扩张(振幅放大,波动加剧)"
else:
contraction_note = "稳定"
s = pivot_state["shift_norm"]
if s > 0.3:
shift_note = "重心上移(偏多)"
elif s < -0.3:
shift_note = "重心下移(偏空)"
else:
shift_note = "重心居中"
msg = (
f"🏠 <b>中枢更新</b> — BTC/USDT 1m\n"
f"\n"
f"📐 笔数: <b>{pivot_state['bi_count']}</b> {dir_label} {phase}\n"
f"📏 收敛率: <b>{pivot_state['contraction']:.4f}</b> → {contraction_note}\n"
f"⚖️ 重心漂移: <b>{pivot_state['shift_norm']:+.4f}</b> → {shift_note}\n"
f"⏱️ 持续: {pivot_state['duration_raw']}K "
f"(norm: {pivot_state['duration_norm']:.2f})\n"
f"📦 区间: {pivot_state['zd']:.2f} {pivot_state['zg']:.2f} "
f"(gg/dd: {pivot_state['gg']:.2f}/{pivot_state['dd']:.2f})"
)
send_telegram_message(msg)
# 3. 检测新 BSP(用 stable key 去重)
current_bsps = engine.bsp_list
current_keys = {_bsp_stable_key(b) for b in current_bsps}
if self._first_run:
self._first_run = False
self._known_bsp_keys = current_keys
confirmed = [b for b in engine.bi_list if b.is_sure]
logger.info(
f"首次运行完成 — {len(confirmed)} 笔已确认, "
f"{len(current_bsps)} 个买卖点 (不推送历史)"
)
elapsed = (time.monotonic() - tick_start) * 1000
logger.info(f"── tick 结束 ({elapsed:.0f}ms) ──")
return
new_keys = current_keys - self._known_bsp_keys
self._known_bsp_keys |= current_keys # 永增,BSP 一旦见过就不会再"新"
if not new_keys:
logger.debug("无新买卖点")
elapsed = (time.monotonic() - tick_start) * 1000
logger.info(f"── tick 结束 ({elapsed:.0f}ms) ──")
return
logger.info(f"检测到 {len(new_keys)} 个新买卖点")
# 4. 推送新 BSP
for bsp in current_bsps:
key = _bsp_stable_key(bsp)
if key not in new_keys:
continue
msg = engine.format_bsp_detail(bsp)
# HTML 转义(Telegram parse_mode=HTML 要求)
msg = msg.replace("&", "&amp;")
msg = msg.replace("<b>", "\x00B\x00").replace("</b>", "\x00/B\x00")
msg = msg.replace("<code>", "\x00C\x00").replace("</code>", "\x00/C\x00")
msg = msg.replace("<", "&lt;").replace(">", "&gt;")
msg = msg.replace("\x00B\x00", "<b>").replace("\x00/B\x00", "</b>")
msg = msg.replace("\x00C\x00", "<code>").replace("\x00/C\x00", "</code>")
ok = send_bsp_alert(msg, bsp_key=key)
if ok:
logger.info(f"✅ 推送: {key}")
else:
logger.warning(f"❌ 推送失败: {key}")
elapsed = (time.monotonic() - tick_start) * 1000
logger.info(f"── tick 结束 ({elapsed:.0f}ms) ──")
async def run(self):
logger.info("=" * 50)
logger.info("bsp_monitor 启动 — BTC/USDT 1m 缠论买卖点监控")
logger.info(f"Telegram: {'已配置' if BOT_TOKEN and CHAT_ID else '⚠️ 未配置'}")
logger.info("=" * 50)
logger.info("首次运行(初始化,不推送)...")
await self.tick()
while True:
now = datetime.now(timezone.utc)
next_minute = now.replace(second=0, microsecond=0) + timedelta(minutes=1)
wait_seconds = max(0.1, (next_minute - now).total_seconds())
logger.info(f"等待 {wait_seconds:.0f}s 到 {next_minute.strftime('%H:%M:%S')}UTC")
await asyncio.sleep(wait_seconds)
try:
await self.tick()
except Exception as e:
logger.error(f"tick 异常: {e}", exc_info=True)
await asyncio.sleep(5)
if __name__ == "__main__":
monitor = BSPMonitor()
try:
asyncio.run(monitor.run())
except KeyboardInterrupt:
logger.info("收到中断信号,退出")