refactor: 缠论引擎迁入 chan/ 分层解耦,指标外置

将核心结构、指标与分析拆到 chan/{core,indicators,analysis,pipeline};
根目录保留兼容 shim;strategies 改为从 chan 包导入;买卖点经 bsp_macd 与 MACD 接合。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Porter
2026-08-03 14:47:13 +08:00
co-authored by Cursor
parent 2e905e7238
commit 2c1232555e
90 changed files with 9067 additions and 8535 deletions
+13
View File
@@ -0,0 +1,13 @@
"""指标外置层:不依赖笔/段/中枢,只产出按 idx 对齐的序列。"""
from .config import IndicatorConfig
from .engine import IndicatorEngine
from .store import IndicatorStore
from .attach import attach_indicators_for_compat
__all__ = [
"IndicatorConfig",
"IndicatorEngine",
"IndicatorStore",
"attach_indicators_for_compat",
]
+20
View File
@@ -0,0 +1,20 @@
"""过渡期:把 IndicatorStore 挂到 KLU 属性上,兼容旧代码读取 klu.ema52 等。"""
from __future__ import annotations
from typing import Iterable
from .store import IndicatorStore
def attach_indicators_for_compat(klu_list: Iterable, store: IndicatorStore) -> None:
"""将指标写入 KLUdeprecated:新代码应通过 store.get(idx, name) 查询)。"""
for klu in klu_list:
idx = getattr(klu, "idx", None)
if idx is None:
continue
row = store.row(idx)
if row is None:
continue
if hasattr(klu, "set_indicators"):
klu.set_indicators(row)
+25
View File
@@ -0,0 +1,25 @@
"""指标参数配置(不再散落在结构流水线内)。"""
from dataclasses import dataclass, field
from typing import List, Tuple
@dataclass
class IndicatorConfig:
macd_fast: int = 26
macd_slow: int = 52
macd_signal: int = 9
ema_periods: Tuple[int, ...] = (5, 7, 10, 13, 24, 26, 52, 104, 156, 208)
rsi_period: int = 14
atr_period: int = 14
# (period, nbdevup, nbdevdn, name_suffix)
bbands: List[Tuple[int, float, float, str]] = field(
default_factory=lambda: [
(365, 3.0, 3.0, "365"),
(120, 3.0, 3.0, "120"),
(20, 2.0, 2.0, "30"),
(20, 2.0, 2.0, "302"),
(26, 3.0, 3.0, "2633"),
]
)
bb_middle_sma_period: int = 90
+74
View File
@@ -0,0 +1,74 @@
"""从 OHLC DataFrame 计算指标,不触碰缠论结构对象。"""
from __future__ import annotations
from typing import Optional
import talib.abstract as ta
import pandas as pd
from .config import IndicatorConfig
from .store import IndicatorStore
def _volume_ratio(df: pd.DataFrame, window: int = 10) -> pd.Series:
vol = df["volume"].astype(float)
ma = vol.rolling(window=window).mean()
ratio = vol / ma
return ratio.fillna(1.0)
class IndicatorEngine:
def __init__(self, config: Optional[IndicatorConfig] = None):
self.config = config or IndicatorConfig()
def compute(self, df: pd.DataFrame, config: Optional[IndicatorConfig] = None) -> IndicatorStore:
cfg = config or self.config
out = df.copy()
fast, slow, period = cfg.macd_fast, cfg.macd_slow, cfg.macd_signal
macd = ta.MACD(out, fastperiod=fast, slowperiod=slow, signalperiod=period)
out["macd"] = macd["macd"]
out["macdsignal"] = macd["macdsignal"]
out["macdhist"] = macd["macdhist"]
for period_n in cfg.ema_periods:
out[f"ema{period_n}"] = ta.EMA(out, timeperiod=period_n)
out["rsi"] = ta.RSI(out, timeperiod=cfg.rsi_period)
out["atr"] = ta.ATR(out, timeperiod=cfg.atr_period)
out["volume_ratio"] = _volume_ratio(out)
bb_middle = ta.SMA(out, timeperiod=cfg.bb_middle_sma_period)
for bb_period, nbup, nbdn, suffix in cfg.bbands:
bb = ta.BBANDS(
out,
timeperiod=bb_period,
nbdevup=nbup,
nbdevdn=nbdn,
matype=0,
)
bbp = (out["close"] - bb["lowerband"]) / (bb["upperband"] - bb["lowerband"])
if suffix == "2633":
out["bb2633upper"] = bb["upperband"]
out["bb2633lower"] = bb["lowerband"]
out["bb2633middle"] = bb["middleband"]
out["bbp2633"] = bbp
elif suffix == "365":
out["bbup365"] = bb["upperband"]
out["bblow365"] = bb["lowerband"]
out["bbp365"] = bbp
elif suffix == "120":
out["bbup120"] = bb["upperband"]
out["bblow120"] = bb["lowerband"]
out["bbp120"] = bbp
elif suffix == "30":
out["bbup30"] = bb["upperband"]
out["bblow30"] = bb["lowerband"]
out["bbmiddle30"] = bb_middle
out["bbp30"] = bbp
elif suffix == "302":
out["bbup302"] = bb["upperband"]
out["bblow302"] = bb["lowerband"]
out["bbp302"] = bbp
return IndicatorStore(out)
+41
View File
@@ -0,0 +1,41 @@
"""按 bar idx 查询指标值。"""
from __future__ import annotations
from typing import Any, Dict, Optional
import pandas as pd
class IndicatorStore:
"""以 DataFrame 列 + 行 idx 对齐的只读指标视图。"""
def __init__(self, df: pd.DataFrame):
self._df = df
@property
def dataframe(self) -> pd.DataFrame:
return self._df
def __len__(self) -> int:
return len(self._df)
def get(self, idx: int, name: str, default: Any = None) -> Any:
if idx < 0 or idx >= len(self._df):
return default
if name not in self._df.columns:
return default
val = self._df.iloc[idx][name]
if pd.isna(val):
return default
return val
def row(self, idx: int) -> Optional[Dict[str, Any]]:
if idx < 0 or idx >= len(self._df):
return None
return self._df.iloc[idx].to_dict()
def series(self, name: str):
if name not in self._df.columns:
return None
return self._df[name]