"""增量更新:新K只追加 KLU/KLC,笔与笔中枢在当前列表上重算。 不改 init_TF_DF 的整段语义。笔必须整表重扫:最后一笔 is_sure 允许收回 (OWN_CHAN_ZS_001 上 60 天出现 7 次)。笔中枢用 cal_bi_zs_list_pure。 """ from __future__ import annotations from datetime import datetime import pandas as pd from pandas import DataFrame from technical.util import resample_to_interval from chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_KLC_STATE from chanlun.core.ChanKLU import ChanKLU class IncrementalBuilderMixin: def init_stream(self, df, interval=1, timeframe=None): """用历史K线初始化流式状态,之后用 append_bar / replace_last_bar。""" if df is None or df.empty: raise ValueError("DataFrame for stream is empty.") if "date" not in df.columns: raise ValueError(f"DataFrame missing 'date' column. Columns: {df.columns.tolist()}") self.timeframe = timeframe self.interval = interval if interval == 1: self.dataframe = df.copy() else: self.dataframe = resample_to_interval(df, interval) self.dataframe = self.add_indicators(self.dataframe) self.klu_list = [] self.klc_list = [] self.bi_list = [] self.bi_zs_list = [] self.seg_list = [] self.zs_list = [] self.bsp_list = [] self.klc_fx_list = [] self.big_zs_list = [] self._klc_feed_last_klu = None for i in range(len(self.dataframe)): self._append_row_at(i, rebuild=False) self.rebuild_bi_zs() return self def append_bar(self, row): """追加一根已收盘K线。同一时间戳则改为替换最后一根。""" self._ensure_stream_state() item = self._normalize_row(row) if self.klu_list and self.klu_list[-1].time == self._row_time_str(item): return self.replace_last_bar(item) self._append_item_to_dataframe(item) self.dataframe = self.add_indicators(self.dataframe) self._append_row_at(len(self.dataframe) - 1, rebuild=True) return self def replace_last_bar(self, row): """更新最后一根K(未完成K线走新OHLC)。包含关系从 KLU 列表重放。""" self._ensure_stream_state() if not self.klu_list: return self.append_bar(row) item = self._normalize_row(row) idx = self.dataframe.index[-1] for key, val in item.items(): self.dataframe.at[idx, key] = val self.dataframe = self.add_indicators(self.dataframe) self._apply_item_to_klu(self.klu_list[-1], self.dataframe.iloc[-1]) self._rebuild_klc_from_klu() self.rebuild_bi_zs() return self def rebuild_bi_zs(self): """在当前 KLC 上重算笔 + cal_bi_zs_list_pure。会先清分型标记。""" self._reset_klc_bi_marks(self.klc_list) self.bi_list = self.cal_bi_list(self.klc_list) if self.klc_list else [] self.bi_zs_list = self.cal_bi_zs_list_pure(self.bi_list) if self.bi_list else [] return self.bi_zs_list def _ensure_stream_state(self): if not hasattr(self, "klu_list") or self.klu_list is None: self.klu_list = [] if not hasattr(self, "klc_list") or self.klc_list is None: self.klc_list = [] if not hasattr(self, "dataframe") or self.dataframe is None: self.dataframe = DataFrame( columns=["date", "open", "high", "low", "close", "volume"] ) if not hasattr(self, "_klc_feed_last_klu"): self._klc_feed_last_klu = self.klu_list[-1] if self.klu_list else None if not hasattr(self, "bi_zs_list"): self.bi_zs_list = [] def _rebuild_klc_from_klu(self): self.klc_list = [] last_klu = None for klu in self.klu_list: self._push_klu_into_klc_list(self.klc_list, klu, last_klu) last_klu = klu self._klc_feed_last_klu = last_klu def _append_row_at(self, idx, rebuild=True): item = self.dataframe.iloc[idx] klu = self._klu_from_item(item, idx) if self.klu_list: self.klu_list[-1].set_next(klu) klu.set_pre(self.klu_list[-1]) self._push_klu_into_klc_list(self.klc_list, klu, self._klc_feed_last_klu) self._klc_feed_last_klu = klu self.klu_list.append(klu) if rebuild: self.rebuild_bi_zs() def _klu_from_item(self, item, idx): klu = ChanKLU( self._item_time_str(item), item["open"], item["high"], item["low"], item["close"], item["volume"], ) klu.set_idx(idx) if not hasattr(klu, "ema13"): klu.ema13 = 0 if "macd" in item: klu.set_indicators(item) return klu def _apply_item_to_klu(self, klu, item): klu.time = self._item_time_str(item) klu.open = item["open"] klu.high = item["high"] klu.low = item["low"] klu.close = item["close"] klu.volume = item["volume"] klu.range = klu.high - klu.low klu.body = abs(klu.close - klu.open) if "macd" in item: klu.set_indicators(item) def _reset_klc_bi_marks(self, klc_list): for klc in klc_list: klc.fx = Chan_FX_TYPE.UNKNOWN klc.klc_fx_type = Chan_KLC_FX.UNKNOWN klc.klc_state = Chan_KLC_STATE.UNKNOWN klc.bi = None klc.fx_confirmed = False def _item_time_str(self, item): date = item["date"] if hasattr(date, "to_pydatetime"): date = date.to_pydatetime() if isinstance(date, datetime): return date.strftime("%Y-%m-%d %H:%M:%S") return str(date) def _row_time_str(self, item): return self._item_time_str(item) def _normalize_row(self, row): if isinstance(row, pd.Series): return row return pd.Series(row) def _append_item_to_dataframe(self, item): row_df = DataFrame([item]) if self.dataframe is None or self.dataframe.empty: self.dataframe = row_df else: self.dataframe = pd.concat([self.dataframe, row_df], ignore_index=True)