第 2 章 · 02 Awesome Oscillator:升级版与反直觉实测 本节摘要:Awesome Oscillator(简称 AO)是 MACD 的"升级版",由 Bill Williams 设计。它做了两件 MACD 没做的事:① 均线不建立在收盘价上,而是建立在 这个"当日公允价"上,窗口用 5/34 两个斐波那契数;② 除了传统的均线交叉信号,还多了一套 saucer 碟形信号——靠三根震荡柱的组合,抢在均线交叉之前下单。本节会精读这套逻辑,并跑一次 MACD vs AO 的对照回测。但本章真正的高潮在最后:作者亲测发现,更灵敏的 AO 并没有比 MACD 赚更多——MACD 的 Sharpe 反而更高,只是最大回撤大一些。
本节摘要:Awesome Oscillator(简称 AO)是 MACD 的"升级版",由 Bill Williams 设计。它做了两件 MACD 没做的事:① 均线不建立在收盘价上,而是建立在
(High+Low)/2这个"当日公允价"上,窗口用 5/34 两个斐波那契数;② 除了传统的均线交叉信号,还多了一套 saucer 碟形信号——靠三根震荡柱的组合,抢在均线交叉之前下单。本节会精读这套逻辑,并跑一次 MACD vs AO 的对照回测。但本章真正的高潮在最后:作者亲测发现,更灵敏的 AO 并没有比 MACD 赚更多——MACD 的 Sharpe 反而更高,只是最大回撤大一些。"faster response may sound awesome, but does not guarantee less risky or more profitable"。这是旅程里第一个让人冷静的反直觉结论。
涉及源码:原项目
Awesome Oscillator backtest.py(334 行),README 第 5 节。
⚠️ 注意:脚本头部
import fix_yahoo_finance as yf需替换为import yfinance as yf。对照回测里 MACD 用 EMA、AO 用 SMA,作者故意做了"平滑方式不同"的对照,读结果时要记住这点差异。
Awesome Oscillator 的公式——
和 MACD 对比,差异只有两处,但每处都意味深长:
差异 1:用 (High+Low)/2 而非 Close。收盘价只是当天最后一笔成交价,容易被尾盘操纵或受单笔大单影响;而 (最高+最低)/2 是当天的"价格中点",更接近真实的供需平衡点(benchmark price)。Bill Williams 认为,基于中点的均线更能反映"市场真正愿意成交的位置"。
差异 2:窗口用 5 和 34(斐波那契数列)。1, 1, 2, 3, 5, 8, 13, 21, 34, 55... 斐波那契数列在技术分析里被赋予了近乎神秘的含义(黄金分割、自然生长律)。5 ≈ 一周交易日,34 ≈ 一个半月,和 MACD 的 12/26 比,窗口更短、反应更快。这种"快"是 AO 的卖点,也是后面反直觉结论的伏笔。
AO 最有特色的是 saucer(碟形)信号。传统均线交叉要等快慢两线相交才下单,而 saucer 只看震荡柱本身的形态,能更早捕捉动量转向。
Bearish Saucer(看空碟形) 的定义(README 引用 TradingView 规则):
AO 在零线之下(即 AO<0);连续两根绿柱(即收盘 > 开盘,当日上涨),且第二根绿柱比第一根更高(AO_{t-1} > AO_{t-2});之后跟着一根红柱(收盘 < 开盘,当日下跌)。三根柱都满足 AO<0。→ 在红柱出现的那一刻下空单。
注意这里的"绿柱/红柱"指的是 K 线本身的颜色(收盘相对开盘涨/跌),而不是震荡柱正负。这有点反直觉:在零线下(整体空头)、但连续两天上涨(绿柱)、且震荡柱在抬升(动量在恢复),然后出现一根红柱(涨势中断)——Bill Williams 认为这是空头重新接管的前兆,所以下空。
Bullish Saucer(看多碟形) 完全镜像:零线之上、连续两根红柱且第二根更深、跟一根绿柱 → 下多。
AO 同时有两套信号源(saucer + 均线交叉),它们可能冲突或重复。作者用一个非常巧妙的 cumsum(累加)机制来仲裁——
如果 saucer 信号先到,那么后续均线交叉产生的"迟到信号"就被忽略;反之亦然。
具体实现是:每次产生信号后,对 awesome signals 列做 cumsum()(累加和),如果累加值说明"已经持有同向仓位",就把新信号清零。这套机制保证同一时刻只持有一次开仓,避免重复下单。这是全书第一次出现 cumsum,后面 Oil Money、Pair Trading 还会反复用到,务必记住。
直觉上,信号越快、抢得越早,应该赚得越多。但金融里有一个永恒的张力——信号速度 vs 信号可靠性。快信号能抓住更多趋势起点,但也更容易被噪声触发假信号;慢信号更可靠,但进场晚、回吐多。AO 用 saucer 抢速度,代价就是假信号变多、交易频次上升(每次交易还要吃无摩擦假设忽略掉的手续费)。这正是后面实测结论的根源。
为了公平对照,作者给 MACD 换了 EMA 指数平滑(上一节入门版用的是 SMA)——
def ewmacd(signals, ma1, ma2): signals['macd ma1'] = signals['Close'].ewm(span=ma1).mean() signals['macd ma2'] = signals['Close'].ewm(span=ma2).mean() return signals
ewm(span=N).mean() 是 pandas 的指数加权平均(EMA),近期价格权重大、反应快。信号生成逻辑与上一节 MACD 完全一致:np.where(ma1>=ma2, 1, 0) 给持仓,.diff() 给下单。
awesome_ma():(H+L)/2 的 SMAdef awesome_ma(signals): signals['awesome ma1'] = ((signals['High'] + signals['Low']) / 2).rolling(window=5).mean() signals['awesome ma2'] = ((signals['High'] + signals['Low']) / 2).rolling(window=34).mean() return signals
一行内做完三件事:算中点 → rolling → mean。5 和 34 写死在函数里(没参数化),这是作者的简化选择。
awesome_signal_generation():saucer + cumsum 仲裁这是全书最值得逐行读懂的函数之一,核心是一个 for 循环——
def awesome_signal_generation(df, method): signals = method(df) signals.reset_index(inplace=True) signals['awesome signals'] = 0 signals['awesome oscillator'] = signals['awesome ma1'] - signals['awesome ma2'] signals['cumsum'] = 0 for i in range(2, len(signals)): # 1) Bearish Saucer(看空碟形) if (signals['Open'][i] > signals['Close'][i] and # 当天红柱 signals['Open'][i-1] < signals['Close'][i-1] and # 前一天绿柱 signals['Open'][i-2] < signals['Close'][i-2] and # 前两天绿柱 signals['awesome oscillator'][i-1] > signals['awesome oscillator'][i-2] and # 震荡柱抬升 signals['awesome oscillator'][i-1] < 0 and # 零线下 signals['awesome oscillator'][i] < 0): # 当天仍在零线下 signals.at[i, 'awesome signals'] = 1 # 2) Bullish Saucer(看多碟形,镜像) if (signals['Open'][i] < signals['Close'][i] and signals['Open'][i-1] > signals['Close'][i-1] and signals['Open'][i-2] > signals['Close'][i-2] and signals['awesome oscillator'][i-1] < signals['awesome oscillator'][i-2] and signals['awesome oscillator'][i-1] > 0 and signals['awesome oscillator'][i] > 0): signals.at[i, 'awesome signals'] = -1 # 3) 均线交叉信号 + cumsum 仲裁 if signals['awesome ma1'][i] > signals['awesome ma2'][i]: signals.at[i, 'awesome signals'] = 1 signals['cumsum'] = signals['awesome signals'].cumsum() if signals['cumsum'][i] > 1: # 已经持多,忽略 signals.at[i, 'awesome signals'] = 0 if signals['awesome ma1'][i] < signals['awesome ma2'][i]: signals.at[i, 'awesome signals'] = -1 signals['cumsum'] = signals['awesome signals'].cumsum() if signals['cumsum'][i] < 0: # 已经持空,忽略 signals.at[i, 'awesome signals'] = 0 signals['cumsum'] = signals['awesome signals'].cumsum() return signals
几个关键点:
reset_index():把日期索引转成 0,1,2... 整数索引,这样 signals['Open'][i-2] 这种位置访问才不会因为索引是 Timestamp 而出错。画图时再 set_index 回时间轴(这种"算完再换回"的技巧,Parabolic SAR 那节也会用)。
saucer 判据是 6 个条件的"与":少一个都不行。注意它用的是 K 线 Open/Close 的颜色判定震荡柱颜色,而不是直接看 awesome oscillator 的正负——这是 AO 的标准画法。
cumsum 仲裁的精妙:第 3 步先无脑把"均线多头排列"的信号设为 1,然后立刻 cumsum() 检查:如果累加和 > 1,说明之前已经开过多仓了(可能由 saucer 抢先),这次均线信号就是迟到的重复,清零。< 0 同理对应空头。这套机制让 saucer 享有优先级,均线信号只能"填补空缺"。
portfolio() / mdd() / stats()作者为了这次对照,特意写了完整回测(平时不写),三个函数——
portfolio() 算资产净值,核心是 cumsum 累计现金流:
capital0 = 5000 # 初始资金 positions = 100 # 每次交易股数 portfolio['awesome holding'] = signals['cumsum'] * portfolio['Close'] * positions # 持仓市值 portfolio['awesome cash'] = capital0 - (signals['awesome signals'] * portfolio['Close'] * positions).cumsum() # 现金 portfolio['awesome asset'] = portfolio['awesome holding'] + portfolio['awesome cash'] # 总资产 = 持仓 + 现金
注意 signals * Close * positions 的物理含义:signals 是 +1/-1/0,乘以收盘价和股数就是"这次买卖花了多少钱"。cumsum() 累加成"累计现金流变化",初始资金减去它就是当前现金。持仓市值则用 cumsum(当前持仓股数)× 价格 × 股数。这套记账法很简洁,后面章节会反复用。
mdd() 算最大回撤,朴素遍历:
def mdd(series): temp = 0 for i in range(1, len(series)): if temp > (series[i] / max(series[:i]) - 1): temp = (series[i] / max(series[:i]) - 1) return temp
对每个时点,用当前资产值除以"此前历史最高"减 1,得到回撤深度(负数),取最负的那个就是最大回撤。max(series[:i]) 是切片取历史最高,虽然是 O(n²),对几百行的回测数据毫无压力。
stats() 算 Sharpe(无风险利率设 0):
stats['awesome sharpe'] = (portfolio['awesome asset'].iloc[-1]/5000 - 1) / np.std(portfolio['awesome return'])
分子是总收益率(期末资产/初始 - 1),分母是日收益率标准差。注意作者用的是"总收益/总波动",而非标准的"年化超额收益/年化波动",这是简化版;真正 Sharpe 还要做年化和无风险利率调整,但作为对照比较够用。

震荡柱按 K 线红绿染色(注意 164 行 c=np.where(new['Open']>new['Close'],'r','g')),正值在上、负值在下,零线穿越和 saucer 形态都能看到。

AO 与 MACD 两条资产曲线叠加,直观看到谁赚得多。注意两条都假设无摩擦,实盘要打折。

awesome ma1/ma2 与 macd ma1/ma2 对照。AO 用 SMA(5/34),MACD 用 EMA(5/34),平滑方式不同。

上下两幅分别是 AO 和 MACD 的持仓标记,箭头数量直观告诉你哪个交易更频繁(AO 通常箭头更密)。
🎯 反直觉发现(本章高潮):作者在脚本末尾留下了他自己的实测结论——
# from my tests # macd has demonstrated a higher sharpe ratio # it executes fewer trades but brings more profits # however its maximum drawdown is higher than awesome oscillator # which one is better? # it depends on your risk averse level翻译:实测中 MACD 的 Sharpe 反而更高,交易次数更少却赚得更多;AO 的最大回撤更小。哪个更好?取决于你的风险厌恶程度。
这非常反直觉——AO 明明"更快、更灵敏、信号更丰富",为什么赚得更少?原因正是前面讲的"信号速度 vs 可靠性"权衡:saucer 抢出来的信号假阳性多,频繁交易吃掉了收益(在无摩擦回测里这点还不明显,实盘加上手续费后差距会更大);MACD 慢归慢,但每次进场都更扎实。
这给旅程种下第一颗批判性思考的种子:"升级版"不一定更好,"更复杂"不一定更赚。一个指标的价值,不取决于它名字有多 awesome,而取决于它的信号在你的标的、你的成本结构、你的风险偏好下,能不能转化为正期望。后面第 3~5 章你会反复看到这个主题的变奏。
(High+Low)/2 作为均线输入:比收盘价更抗操纵,反映当日公允价,是 Bill Williams 系列指标的标配。cumsum 信号仲裁:多个信号源时,用累加和判断"是否已持仓",已持仓则忽略新信号。这是处理多源冲突的通用技巧。reset_index() 便于整数索引递归:signals['col'][i-1] 这种位置访问要求整数索引,算完再 set_index 回时间轴画图。signals * Close * positions 算单笔现金流,cumsum 算累计,初始资金减它得当前现金,加持仓市值得总资产。总收益 / std(日收益),无年化无风险利率调整,仅作对照比较用。(High+Low)/2 上,窗口用 5/34 斐波那契数。下一节,我们离开均线世界,看 Welles Wilder 发明的递归止盈止损指标——Parabolic SAR。