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:
@@ -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",
|
||||
]
|
||||
@@ -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:
|
||||
"""将指标写入 KLU(deprecated:新代码应通过 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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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]
|
||||
Reference in New Issue
Block a user