Files
Chan/ChanPivotMonitor.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

145 lines
4.9 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.
"""
实时中枢特征跟踪器
Real-time Pivot Feature Tracker
定位: 观察者 — 不修改管线,只观察 bi_zs_list 中当前中枢的特征变化。
每次管线重算后调用 update(),检测 bi_count 是否增长,若增长则重新计算
shift / contraction / duration。
"""
from collections import deque
from ChanPivotClassifier import ChanPivotClassifier
class ChanPivotMonitor:
"""
实时追踪当前中枢的结构特征。
update() 每次管线重算后调用,对比 bi_count 判断是否有新笔加入中枢。
若 bi_count 增长则重新计算 3 个结构特征并返回最新值。
"""
def __init__(self, window_size: int = 10):
self._window_size = window_size
self._duration_history: deque[int] = deque(maxlen=window_size)
self._current_zs_id: tuple | None = None
self._current_bi_count: int = 0
self._current_is_sure: bool = False
self._current_state: dict | None = None
self._duration_added_for_zs: set = set() # 已加入窗口的中枢 ID
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def update(self, bi_zs_list: list) -> dict | None:
"""
主入口:检测当前中枢特征变化。
参数:
bi_zs_list: 当前管线产出的笔中枢列表
返回:
特征 dict(有变化时),无变化返回 None
"""
if not bi_zs_list:
self._current_zs_id = None
self._current_bi_count = 0
self._current_is_sure = False
self._current_state = None
return None
zs = self._find_current_zs(bi_zs_list)
if zs is None:
return None
zs_id = self._make_zs_id(zs)
bi_count = len(zs.bi_list)
is_sure = zs.is_sure
# 无变化 → 跳过
if (zs_id == self._current_zs_id
and bi_count == self._current_bi_count
and is_sure == self._current_is_sure):
return None
# 中枢切换 → 将旧中枢 duration 加入窗口
if zs_id != self._current_zs_id:
self._maybe_add_to_history()
self._current_zs_id = zs_id
self._current_bi_count = bi_count
self._current_is_sure = is_sure
features = ChanPivotClassifier.compute_features(
zs, list(self._duration_history)
)
self._current_state = {
"zs_id": zs_id,
"zs_index": zs.index,
"zs_dir": str(zs.dir),
"bi_count": bi_count,
"is_sure": zs.is_sure,
"zg": round(zs.zg, 6),
"zd": round(zs.zd, 6),
"gg": round(zs.gg, 6),
"dd": round(zs.dd, 6),
**features,
"start_time": str(zs.start_time) if hasattr(zs, "start_time") and zs.start_time else None,
}
# 中枢刚变为已确认时,将其 duration 加入滚动窗口
if is_sure and zs_id not in self._duration_added_for_zs:
self._add_duration(features["duration_raw"])
self._duration_added_for_zs.add(zs_id)
return self._current_state
def get_current(self) -> dict | None:
"""返回当前中枢的最新特征"""
return self._current_state
def get_duration_history(self) -> list[int]:
"""返回用于归一化的 duration 滚动窗口"""
return list(self._duration_history)
# ------------------------------------------------------------------
# Internal
# ------------------------------------------------------------------
@staticmethod
def _make_zs_id(zs) -> tuple:
"""生成中枢的稳定标识(基于首笔首K线索引,不依赖 zs.index"""
bi0 = zs.bi_list[0]
return (bi0.start_klc.index,)
@staticmethod
def _find_current_zs(bi_zs_list: list):
"""
找到当前活跃中枢:
优先取最后一个 is_sure=False(形成中)的中枢,
没有则取最后一个 is_sure=True 的中枢。
"""
forming = None
last_sure = None
for zs in bi_zs_list:
if len(zs.bi_list) < 3:
continue
if not zs.is_sure:
forming = zs
else:
last_sure = zs
return forming if forming is not None else last_sure
def _add_duration(self, duration_raw: int):
"""将已确认中枢的 duration 加入滚动窗口"""
self._duration_history.append(duration_raw)
def _maybe_add_to_history(self):
"""旧中枢切换前,若已确认且未记录过,则将其 duration 加入窗口"""
if (self._current_state and self._current_state["is_sure"]
and self._current_zs_id not in self._duration_added_for_zs):
self._add_duration(self._current_state["duration_raw"])
self._duration_added_for_zs.add(self._current_zs_id)