chanlun/indicators/ta.py 接口兼容 talib.abstract,实现代码实际用到的 SMA/MA/EMA/RSI/ATR/MACD/BBANDS;chanlun/pipeline/resample.py 替代 technical.util.resample_to_interval。调用点只改 import,逻辑未动。 暖机长度与平滑种子按 TA-Lib 的约定实现,差一根 K 线就会让下游所有 笔/线段/中枢整体位移。其中 MACD 需特别处理:TA-Lib 让快慢两条 EMA 在同一根 K 线出首值,因而快线的种子取 x[slow-fast:slow] 的均值,而非 从 fastperiod-1 一路递推——两者在百元价位上相差约 0.17。 BBANDS 是有意的分歧:TA-Lib 用 sumsq/n - mean² 求方差,短窗口远离零 时灾难性抵消(timeperiod=2 误差 8.7e-7),本实现用 rolling std,对 50 位精度基准误差为 0。项目实际使用的周期两者一致到 1e-10。 顺带清理 12 个文件中 16 处从未调用的 talib/technical 导入。 验证:9440 组随机对拨;真实 K 线端到端比对 add_indicators 全部 33 个 指标列,NaN 模式一致、MACD 柱符号 100% 相同;屏蔽两个包后 60 个模块 均可导入。新增 test_ta_compat.py 将输出逐 bar 钉在 TA-Lib 上,但该文件 在 TA-Lib 缺失时静默跳过,改动 ta.py 需在装有 TA-Lib 的环境复跑。 Co-authored-by: Cursor <cursoragent@cursor.com>
172 lines
5.4 KiB
Python
172 lines
5.4 KiB
Python
"""增量更新:新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 chanlun.pipeline.resample 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)
|