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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
5ad761fad4
commit
b1cbdca707
+20
-26
@@ -1,45 +1,39 @@
|
||||
"""
|
||||
fetcher.py - CCXT REST 拉取 Binance 永续合约 1m K 线,从固定起点累积。
|
||||
fetcher.py - 从 data_provider HTTP API 拉取 K 线数据。
|
||||
"""
|
||||
import ccxt
|
||||
|
||||
import requests
|
||||
import pandas as pd
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SYMBOL = "BTC/USDT:USDT"
|
||||
TIMEFRAME = "1m"
|
||||
# 每次拉取最近 LIMIT 根 K 线(Binance 上限 1500,足够缠论管线用 ~25h 数据)
|
||||
_FETCH_LIMIT = 1000
|
||||
|
||||
_exchange = None
|
||||
|
||||
|
||||
def _get_exchange():
|
||||
global _exchange
|
||||
if _exchange is None:
|
||||
_exchange = ccxt.binance({
|
||||
"enableRateLimit": True,
|
||||
"options": {"defaultType": "future"},
|
||||
})
|
||||
_exchange.load_markets()
|
||||
logger.info("ccxt binance 已初始化")
|
||||
return _exchange
|
||||
PROVIDER_URL = "http://103.179.242.166"
|
||||
FETCH_LIMIT = 1000
|
||||
|
||||
|
||||
def fetch_ohlcv() -> pd.DataFrame:
|
||||
"""拉取最近 _FETCH_LIMIT 根 1m K 线。
|
||||
"""从 data_provider API 拉取最近 FETCH_LIMIT 根 1m K 线。"""
|
||||
url = f"{PROVIDER_URL}/api/candles"
|
||||
params = {
|
||||
"symbol": SYMBOL,
|
||||
"tf": TIMEFRAME,
|
||||
"limit": FETCH_LIMIT,
|
||||
}
|
||||
resp = requests.get(url, params=params, timeout=30)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
管线每次重跑最新的 K 线窗口。
|
||||
用 limit 而非 since 避免 API 500 根限制截断新数据。
|
||||
"""
|
||||
exchange = _get_exchange()
|
||||
raw = exchange.fetch_ohlcv(SYMBOL, TIMEFRAME, limit=_FETCH_LIMIT)
|
||||
if not data:
|
||||
logger.warning("API 返回空数据")
|
||||
return pd.DataFrame()
|
||||
|
||||
df = pd.DataFrame(raw, columns=["timestamp", "open", "high", "low", "close", "volume"])
|
||||
df = pd.DataFrame(data)
|
||||
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
df["date"] = df["timestamp"]
|
||||
|
||||
df = df.drop_duplicates(subset="timestamp").sort_values("timestamp").reset_index(drop=True)
|
||||
logger.info(f"拉取 {len(df)} 根 {TIMEFRAME} K 线 from {PROVIDER_URL}")
|
||||
return df
|
||||
|
||||
+57
-1
@@ -19,7 +19,12 @@ 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, BOT_TOKEN, CHAT_ID
|
||||
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,
|
||||
@@ -38,6 +43,7 @@ class BSPMonitor:
|
||||
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。"""
|
||||
@@ -68,6 +74,56 @@ class BSPMonitor:
|
||||
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}
|
||||
|
||||
@@ -54,6 +54,39 @@ def _save_pushed():
|
||||
_load_pushed()
|
||||
|
||||
|
||||
def send_telegram_message(text: str) -> bool:
|
||||
"""发送 Telegram 消息(不去重,每次调用都发)。
|
||||
|
||||
Args:
|
||||
text: HTML 格式的消息文本
|
||||
|
||||
Returns:
|
||||
True 如果发送成功
|
||||
"""
|
||||
if not BOT_TOKEN or not CHAT_ID:
|
||||
logger.warning("Telegram 未配置,跳过推送")
|
||||
return False
|
||||
|
||||
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
|
||||
try:
|
||||
resp = requests.post(
|
||||
url,
|
||||
json={
|
||||
"chat_id": CHAT_ID,
|
||||
"text": text,
|
||||
"parse_mode": "HTML",
|
||||
"disable_web_page_preview": True,
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
logger.info(f"Telegram 推送成功")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Telegram 推送失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def send_bsp_alert(text: str, bsp_key: str = "") -> bool:
|
||||
"""通过 Telegram Bot API 推送买卖点消息(自动去重)。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user