- 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>
145 lines
4.9 KiB
Python
145 lines
4.9 KiB
Python
"""
|
||
实时中枢特征跟踪器
|
||
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)
|