第 9 章 · 02 IndicatorBu 弃用 talib 自实现指标 本节摘要:本节解剖 ABU 的技术指标库—— (约 970 行,5 个指标模块)。ABU 在这里做了一个果断的架构决策:彻底弃用 talib( 里 写死),全部用 pandas 自实现 ATR/MA/EMA/布林/MACD/RSI。原因有三:talib 跨平台安装困难(尤其 Windows,需要编译 C 库)、自实现便于教学(每行公式都看得见)、双引擎架构保留了切换能力(每个指标的 函数仍在,改一行 gcalctype 就能切回 talib)。
本节摘要:本节解剖 ABU 的技术指标库——
IndicatorBu(约 970 行,5 个指标模块)。ABU 在这里做了一个果断的架构决策:彻底弃用 talib(ABuNDBase.py里g_calc_type = ECalcType.E_FROM_PD写死),全部用 pandas 自实现 ATR/MA/EMA/布林/MACD/RSI。原因有三:talib 跨平台安装困难(尤其 Windows,需要编译 C 库)、自实现便于教学(每行公式都看得见)、双引擎架构保留了切换能力(每个指标的_calc_xxx_from_ta函数仍在,改一行 g_calc_type 就能切回 talib)。本节先拆ABuNDBase的统一架构(ECalcType 枚举 + plot_from_order 通用绘制流程),再逐一精读 5 个指标的"_from_ta + _from_pd + calc_xxx + plot 四件套"统一模式。重点看 ATR——源码注释坦承"没有完全按照标准公式使用简单移动平均,使用了 pd_ewm_mean 即加权移动平均",这是 ABU 对经典指标的有意改写。读完本节,你将理解 ABU"不依赖 talib 也能算全部常用指标"的工程取舍。
内容来源:原项目源码
abupy/IndicatorBu/ABuNDBase.py+ABuNDAtr.py+ABuNDMa.py+ABuNDBoll.py+ABuNDMacd.py+ABuNDRsi.py(共约 970 行)。
⚠️ 注意:本节重点理解"双引擎架构"。ABUNDBase 用一个模块级变量
g_calc_type决定全局用 talib 还是 pandas,每个指标模块的calc_xxx = _calc_xxx_from_pd if g_calc_type == E_FROM_PD else _calc_xxx_from_ta是统一的分发模式。虽然实际写死了 E_FROM_PD,但_from_ta函数都还在(完整可运行),这是"以备不时之需"的工程冗余。另外 ATR 的 ewm 改写是已知偏差——如果你要把 ABU 的 ATR 与 talib 的 ATR 对比,会有数值差异。
阅读完本节,你应当能够:
g_calc_type = E_FROM_PD 写死的三条原因(跨平台安装难/便于教学/双引擎保留切换)。plot_from_order 的通用绘制流程:order → 确定 symbol/buy_date/sell_date → date_ext 推算 start/end → make_kl_df 取 KL → plot_nd_func 绘制。_calc_xxx_from_ta + _calc_xxx_from_pd + calc_xxx(分发)+ plot_xxx(绘制)。_calc_atr_from_pd:shift(1) 得昨收 → TR=max(∣H-L∣,∣H-昨收∣,∣昨收-L∣) → pd_ewm_mean 加权(非标准简单移动平均),以及为什么 ABU 故意用 ewm。_calc_ma_from_pd 与 calc_ma_from_prices 的区别(后者多了 min_periods 参数,被 AbuDoubleMaBuy 调用)。_calc_rsi_from_pd 的 5 步:diff → gain/loss 分离 → N 日均值 → rs=gain/loss → rsi=100-100/(1+rs)。ABuNDBase.py(106 行)是指标库的基础设施。核心是 ECalcType 枚举和全局分发变量:
21 class ECalcType(Enum): 25 """使用talib透传技术指标计算""" 26 E_FROM_TA = 0 28 """使用pandas等库实现技术指标计算""" 29 E_FROM_PD = 1 39 """彻底不用talib,完全都使用自己计算的指标结果""" 40 g_calc_type = ECalcType.E_FROM_PD
E_FROM_TA 表示用 talib(透传 talib.ATR/talib.MA 等),E_FROM_PD 表示用 pandas 自实现。源码注释"彻底不用 talib,完全都使用自己计算的指标结果"——这是 ABU 的明确决策。
源码上方注释(L31-38)保留了一段被注释掉的"自动探测"逻辑:
31 # try: 32 # # 不强制要求talib,全部局部引用 33 # # noinspection PyUnresolvedReferences 34 # import talib 35 # g_calc_type = ECalcType.E_FROM_TA 36 # except ImportError: 37 # # 没有安装talib,使用E_FROM_PD 38 # g_calc_type = ECalcType.E_FROM_PD
原本设计是"有 talib 就用 talib,没有就退回 pandas"。但最终写死了 E_FROM_PD,原因:
pip install 即可。_calc_xxx_from_pd 都是几十行 pandas 代码,公式一目了然。talib 是黑盒,读者看不到 TR 怎么算、EMA 怎么加权。ABU 是教程项目,透明度优先。_calc_xxx_from_ta 函数全部保留且可运行(都 import talib),只要把 g_calc_type 改成 E_FROM_TA 就切回 talib。这是"以备不时之需"的工程冗余——如果未来某指标 pandas 实现性能不够,可以单独切。plot_from_order(ABuNDBase.py:43-105)是所有指标绘制的基础设施——给定一个 order(交易记录),自动取 KL 数据并绘制指标 + 标注买卖点:
68 def plot_from_series(p_order): 74 target_symbol = p_order['symbol'] 77 buy_index = pd.to_datetime(str(p_order['buy_date'])) 80 start = ABuDateUtil.begin_date(date_ext, date_str=start, fix=False) # 向前推 date_ext 天 83 if p_order['sell_type'] != 'keep': 84 sell_index = pd.to_datetime(str(p_order['sell_date'])) 88 end = ABuDateUtil.begin_date(-date_ext, date_str=end, fix=False) # 向后推 93 kl_pd = ABuSymbolPd.make_kl_df(target_symbol, start=start, end=end) 98 return plot_nd_func(kl_pd, with_points=buy_index, with_points_ext=sell_index, **kwargs)
date_ext 默认 120——围绕交易日期前后各取 120 天,保证图表能看到完整的交易上下文。plot_nd_func 是具体的指标绘制函数(plot_atr/plot_ma/...),由调用方传入。这就是为什么每个指标模块都有 plot_xxx_from_order:它就是 plot_from_order(plot_xxx_from_klpd, ...) 的一行封装。
单子(order 只有 1 行)与多子(DataFrame)的处理在 L59-66:单行 DataFrame 自动转 Series,统一走 plot_from_series;多子用 apply(plot_from_series, axis=1) 逐行绘制。
ABuNDAtr.py 的 _calc_atr_from_pd(ABuNDAtr.py:51-85)是本节最该精读的函数:
51 def _calc_atr_from_pd(high, low, close, time_period=14): 60 if isinstance(close, pd.Series): 62 pre_close = close.shift(1).values # shift(1) 得昨收 64 pre_close = shift(close, 1) 67 pre_close[0] = pre_close[1] # 首元素无昨收,用第二天填 74 tr_hl = np.abs(high - low) # ∣最高 - 最低∣ 77 tr_hc = np.abs(high - pre_close) # ∣最高 - 昨收∣ 79 tr_cl = np.abs(pre_close - low) # ∣昨收 - 最低∣ 81 tr = np.maximum(np.maximum(tr_hl, tr_hc), tr_cl) # TR = 三者最大 83 # (ATR)= MA(TR, N), 这里没有完全按照标准公式使用简单移动平均,使用了pd_ewm_mean,即加权移动平均 84 atr = pd_ewm_mean(pd.Series(tr), span=time_period, min_periods=1) 85 return atr.values
五步:
pd_ewm_mean(指数加权移动平均)。ewm 给近期数据更大权重,对波动变化反应更快。calc_atr = _calc_atr_from_pd if g_calc_type == E_FROM_PD else _calc_atr_from_ta。L82-83 的注释是 ABU 唯一公开承认的"对经典指标的改写"——作者特意标注"没有完全按照标准公式使用简单移动平均"。这种诚实态度在量化源码里少见。ewm vs SMA 的差异:ewm 反应快但噪声大,SMA 平滑但滞后。ABU 选 ewm,可能是因为它用在卖出因子的止损止盈上,需要快速感知波动变化。
atr14/atr21(L91-112)是固定周期的便捷封装(N=14 和 N=21,14 是 Wilder 原版,21 是月线)。atr14_min/atr14_max 等是确定常数阈值时用的(取序列最小/最大,fillna 后算)。
ABuNDMa.py 的 _calc_ma_from_pd(L56-71)与 calc_ma_from_prices(L74-91):
56 def _calc_ma_from_pd(prices, time_period=10, from_calc=EMACalcType.E_MA_MA): 64 if isinstance(prices, pd.Series): 65 prices = prices.values 67 if from_calc == EMACalcType.E_MA_MA: 68 ma = pd_rolling_mean(prices, window=time_period, min_periods=time_period) # min_periods=window 70 ma = pd_ewm_mean(prices, span=time_period, min_periods=time_period) 74 def calc_ma_from_prices(prices, time_period=10, min_periods=None, from_calc=EMACalcType.E_MA_MA): 86 min_periods = time_period if min_periods is None else min_periods 87 if from_calc == EMACalcType.E_MA_MA: 88 ma = pd_rolling_mean(prices, window=time_period, min_periods=min_periods) # min_periods 可调 90 ma = pd_ewm_mean(prices, span=time_period, min_periods=min_periods)
两个函数的区别:calc_ma_from_prices 多了 min_periods 参数。
_calc_ma_from_pd:min_periods=time_period,意思是"窗口不满 N 天就返回 NaN"。比如算 MA60,前 59 天都是 NaN,第 60 天才有值。这是严格定义。calc_ma_from_prices:min_periods 可外部设置。如果设 min_periods=1,窗口不满也能算(用部分数据)。比如 MA60 在第 1 天就用当天价格当 MA60,第 2 天用 2 天均价,...,第 60 天才是真正的 60 日均价。calc_ma_from_prices 被 AbuDoubleMaBuy(双均线买入因子)和第 8 章 BtcBigWaveClf(比特币大波动)调用——回测早期数据不足时,strict MA 全是 NaN 会让因子发不出信号,min_periods=1 保证早期也能算。这是 ABU 处理"冷启动"的标准手法。
EMACalcType 枚举(L28-32):E_MA_MA(简单移动平均)/ E_MA_EMA(指数加权)。两个都在 ewm_mean 和 rolling_mean 之间切换。
ABuNDBoll.py 的 _calc_boll_from_pd(L52-70):
61 middle = pd_rolling_mean(prices, window=time_period, min_periods=time_period) # 中轨 = N 日 MA 63 n_std = pd_rolling_std(prices, window=20, center=False) # N 日移动标准差 65 upper = middle + nb_dev * n_std # 上轨 = 中轨 + nb_dev × std 67 lower = middle - nb_dev * n_std # 下轨 = 中轨 - nb_dev × std 70 return upper.values, middle.values, lower.values
三轨公式(注释 L9-12):
注意 L63 的 window=20 是硬编码——即使 time_period 不是 20,std 的窗口仍是 20。这可能是源码笔误(应该用 time_period),但因为是教学示例,影响不大。布林带的核心思想:价格在上下轨之间波动,带宽随波动率变化(波动大涨得猛,带宽变宽;盘整时带宽变窄)。突破上轨是强势,跌破下轨是弱势。
ABuNDMacd.py 的 _calc_macd_from_pd(L50-71):
63 ewma_fast = pd_ewm_mean(price, span=fast_period) # EMA12 快线 65 ewma_slow = pd_ewm_mean(price, span=slow_period) # EMA26 慢线 67 dif = ewma_fast - ewma_slow # DIF = 快 - 慢 69 dea = pd_ewm_mean(dif, span=signal_period) # DEA = DIF 的 9 日 EMA(信号线) 70 bar = (dif - dea) # MACD 柱 = DIF - DEA 71 return dif, dea, bar
MACD 三件套:
注释(L7-9)说"由快的加权移动均线(EMA12)减去慢的加权移动均线(EMA26)得到 DIF,再用 DIF - (快线-慢线的 9 日加权移动均线 DEA)得到 MACD 柱"——这就是经典 MACD 定义,ABU 实现忠实于公式。
ABuNDRsi.py 的 _calc_rsi_from_pd(L51-79):
62 if g_rsi_gain: 64 diff_price = prices.diff() # 用 diff(价格变动绝对值) 66 diff_price = prices.pct_change() # 或用 pct_change(变动比例) 68 diff_price[0] = 0 # 首元素归零 71 gain = np.where(diff_price > 0, diff_price, 0) # 涨日变动,跌日归零 72 loss = np.where(diff_price < 0, abs(diff_price), 0) # 跌日变动(取绝对值),涨日归零 74 gain_mean = pd_rolling_mean(gain, window=time_period) # N 日平均涨幅 75 loss_mean = pd_rolling_mean(loss, window=time_period) # N 日平均跌幅 77 rs = gain_mean / loss_mean # RS = 平均涨幅 / 平均跌幅 78 rsi = 100 - 100 / (1 + rs) # RSI = 100 - 100/(1+RS)
五步:
diff()(默认)或 pct_change()(由 g_rsi_gain 控制,默认 True 用 diff)。gain_mean / loss_mean。100 - 100 / (1 + rs)。RSI 在 0-100 之间。RSI>70 通常认为超买(该跌),RSI<30 超卖(该涨)。注释(L7-14)完整列出了这 5 步,是 ABU 教学风格的体现——公式写在 docstring 里,代码忠实实现。
5 个指标模块(ABuNDAtr/ABuNDMa/ABuNDBoll/ABuNDMacd/ABuNDRsi)都遵循同一个"四件套"模式:
| 件 | 命名 | 作用 |
|---|---|---|
| 1 | _calc_xxx_from_ta |
talib 实现(import talib,透传 talib.ATR 等) |
| 2 | _calc_xxx_from_pd |
pandas 实现(纯 numpy/pandas,公式透明) |
| 3 | calc_xxx |
分发变量(= _from_pd if g_calc_type == E_FROM_PD else _from_ta) |
| 4 | plot_xxx / plot_xxx_from_klpd / plot_xxx_from_order |
三层绘制封装(原始/from kl_pd/from order) |
绘制层的三级嵌套:
plot_xxx:最底层,接收原始数组(high/low/close/index),画图。plot_xxx_from_klpd:接收 kl_pd DataFrame,提取列后调 plot_xxx。plot_xxx_from_order:接收 order,通过 plot_from_order 自动取 KL 后调 plot_xxx_from_klpd。这种嵌套让用户可以从任何层级调用——有原始数据用 plot_xxx,有 kl_pd 用 from_klpd,有交易记录用 from_order(自动标注买卖点)。这是 ABU API 设计的统一美感。
g_calc_type = E_FROM_PD 写死,彻底不用 talib。原因:跨平台安装难(Windows 编译 C 库)/ 便于教学(公式透明)/ 双引擎保留切换(_from_ta 函数仍在)。_calc_xxx_from_ta + _calc_xxx_from_pd + calc_xxx(分发)+ plot_xxx/plot_xxx_from_klpd/plot_xxx_from_order(三级绘制)。_calc_ma_from_pd 用 min_periods=window(严格,早期 NaN),calc_ma_from_prices 可外部设 min_periods=1(冷启动可用部分数据)。后者被 AbuDoubleMaBuy 和 BtcBigWaveClf 调用。下一章(第 10 章)将进入并行调度——ABU 怎么用 Master-Worker 三层架构把全市场几百只股票的回测并行跑起来。