""" 实时中枢特征跟踪器 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)