The Awesome Oscillator (AO), developed by Bill
Williams, is a momentum indicator that reflects changes in market
driving force. One of its key patterns is the “Saucer,”
which signals a short-term shift in momentum within an established
trend, suggesting a potential continuation of that trend. This article
will explain the AOSaucerStrategy, which aims to capitalize
on these patterns, filtered by a long-term Exponential Moving Average
(EMA) and protected by a dynamic ATR-based trailing stop.
The AOSaucerStrategy identifies entries based on
specific Awesome Oscillator configurations that resemble a
“saucer” shape, signaling a brief pause or reversal in short-term
momentum before the dominant trend resumes. To ensure trades align with
the larger market direction, an
Exponential Moving Average (EMA) acts as a trend filter.
Finally, an Average True Range (ATR) based trailing stop is
implemented for robust risk management.
A bullish saucer pattern indicates a potential buying opportunity in an uptrend. It’s identified when:
Essentially, it looks for two consecutive red bars in the AO histogram followed by a green bar, all while the AO remains above zero, and price is in an uptrend.
A bearish saucer pattern suggests a potential selling opportunity in a downtrend. It’s identified when:
This involves two consecutive green bars in the AO histogram followed by a red bar, all while the AO remains below zero, and price is in a downtrend.
The strategy is implemented in backtrader as
follows:
import backtrader as bt
class AOSaucerStrategy(bt.Strategy):
"""
A trend-continuation strategy that enters on a Bill Williams 'Saucer'
pattern in the Awesome Oscillator, filtered by a long-term EMA.
"""
params = (
# Awesome Oscillator
('ao_fast', 7),
('ao_slow', 30),
# Trend Filter
('trend_period', 30),
# Risk Management
('atr_period', 14),
('atr_stop_multiplier', 3.0),
)
def __init__(self):
self.order = None
# --- Indicators ---
self.ao = bt.indicators.AwesomeOscillator(self.data, fast=self.p.ao_fast, slow=self.p.ao_slow)
self.trend_ema = bt.indicators.ExponentialMovingAverage(self.data, period=self.p.trend_period)
self.atr = bt.indicators.AverageTrueRange(self.data, period=self.p.atr_period)
# --- Trailing Stop State ---
self.stop_price = None
self.highest_price_since_entry = None
self.lowest_price_since_entry = None
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]: return
if order.status in [order.Completed]:
if self.position and self.stop_price is None:
if order.isbuy():
self.highest_price_since_entry = self.data.high[0]
self.stop_price = self.highest_price_since_entry - (self.atr[0] * self.p.atr_stop_multiplier)
elif order.issell():
self.lowest_price_since_entry = self.data.low[0]
self.stop_price = self.lowest_price_since_entry + (self.atr[0] * self.p.atr_stop_multiplier)
elif not self.position:
self.stop_price = None; self.highest_price_since_entry = None; self.lowest_price_since_entry = None
self.order = None
def next(self):
if self.order: return
if not self.position:
# --- 1. Check Macro Trend Filter ---
is_uptrend = self.data.close[0] > self.trend_ema[0]
is_downtrend = self.data.close[0] < self.trend_ema[0]
# --- 2. Check for Saucer Patterns ---
# Bullish Saucer requires AO > 0
if is_uptrend and self.ao[0] > 0:
is_green_bar = self.ao[0] > self.ao[-1]
is_second_red_bar_smaller = self.ao[-1] < 0 and self.ao[-1] > self.ao[-2]
if is_green_bar and is_second_red_bar_smaller:
self.order = self.buy()
# Bearish Saucer requires AO < 0
elif is_downtrend and self.ao[0] < 0:
is_red_bar = self.ao[0] < self.ao[-1]
is_second_green_bar_smaller = self.ao[-1] > 0 and self.ao[-1] < self.ao[-2]
if is_red_bar and is_second_green_bar_smaller:
self.order = self.sell()
elif self.position:
# --- Manual ATR Trailing Stop Logic ---
if self.position.size > 0: # Long
self.highest_price_since_entry = max(self.highest_price_since_entry, self.data.high[0])
new_stop = self.highest_price_since_entry - (self.atr[0] * self.p.atr_stop_multiplier)
self.stop_price = max(self.stop_price, new_stop)
if self.data.close[0] < self.stop_price: self.order = self.close()
elif self.position.size < 0: # Short
self.lowest_price_since_entry = min(self.lowest_price_since_entry, self.data.low[0])
new_stop = self.lowest_price_since_entry + (self.atr[0] * self.p.atr_stop_multiplier)
self.stop_price = min(self.stop_price, new_stop)
if self.data.close[0] > self.stop_price: self.order = self.close()params)ao_fast, ao_slow: Periods for the Awesome
Oscillator calculation.trend_period: Period for the Exponential Moving
Average, used to determine the primary trend.atr_period, atr_stop_multiplier:
Parameters for the ATR-based trailing stop, controlling its
sensitivity.__init__)The __init__ method sets up the necessary
indicators:
self.ao: The Awesome Oscillator
indicator.self.trend_ema: The Exponential Moving
Average for trend filtering.self.atr: The Average True Range for
calculating the trailing stop.self.stop_price,
self.highest_price_since_entry,
self.lowest_price_since_entry: Variables to manage the
dynamic ATR trailing stop.notify_order)This method is crucial for handling order completions and managing
the trailing stop. When a position is entered, it sets the initial
stop_price based on the entry price and current ATR. When a
position is closed, it resets the trailing stop variables, preparing for
the next trade.
next)The next method is executed bar by bar and contains the
core trading logic:
is_uptrend) or below
(is_downtrend) the trend_ema. This ensures
trades are only taken in the direction of the macro trend.AO is positive, it looks for the specific pattern:
current AO bar is green, and the previous AO
bar was red and smaller than the one before it. If this
saucer formation is found, a buy order is
placed.AO is negative, it looks for the inverse pattern:
current AO bar is red, and the previous AO bar
was green and smaller than the one before it. If this
saucer formation is found, a sell order is
placed.stop_price.
stop_price
moves up as the price moves higher, locking in profits. If the price
falls below the trailing stop, the position is closed.stop_price
moves down as the price moves lower. If the price rises above the
trailing stop, the position is closed. This manual implementation
ensures trailing stops are always active and adjust dynamically.To evaluate the strategy’s performance, we’ll employ a rolling backtest. This method assesses the strategy over multiple, successive time windows, providing a more robust view of its consistency compared to a single, fixed backtest.
from collections import deque
import backtrader as bt
import numpy as np
import pandas as pd
import yfinance as yf
import dateutil.relativedelta as rd
def run_rolling_backtest(
ticker,
start,
end,
window_months,
strategy_class,
strategy_params=None
):
strategy_params = strategy_params or {}
all_results = []
start_dt = pd.to_datetime(start)
end_dt = pd.to_datetime(end)
current_start = start_dt
while True:
current_end = current_start + rd.relativedelta(months=window_months)
# Ensure the current_end does not exceed the overall end date
if current_end > end_dt:
current_end = end_dt # Adjust current_end to the overall end_dt if it overshoots
if current_start >= current_end: # If the window becomes invalid, break
break
print(f"\nROLLING BACKTEST: {current_start.date()} to {current_end.date()}")
# Fetch data using yfinance
data = yf.download(ticker, start=current_start, end=current_end, auto_adjust=False, progress=False)
if data.empty or len(data) < 90:
print("Not enough data for this period. Skipping.")
# Advance start date to the next window for the next iteration
current_start += rd.relativedelta(months=window_months)
continue
if isinstance(data.columns, pd.MultiIndex):
data = data.droplevel(1, 1)
# Calculate Buy & Hold return for the period
start_price = data['Close'].iloc[0]
end_price = data['Close'].iloc[-1]
benchmark_ret = (end_price - start_price) / start_price * 100
feed = bt.feeds.PandasData(dataname=data)
cerebro = bt.Cerebro()
cerebro.addstrategy(strategy_class, **strategy_params)
cerebro.adddata(feed)
cerebro.broker.setcash(100000)
cerebro.broker.setcommission(commission=0.001)
cerebro.addsizer(bt.sizers.PercentSizer, percents=95)
start_val = cerebro.broker.getvalue()
try:
cerebro.run()
except Exception as e:
print(f"Error running backtest for {current_start.date()} to {current_end.date()}: {e}")
current_start += rd.relativedelta(months=window_months)
continue
final_val = cerebro.broker.getvalue()
strategy_ret = (final_val - start_val) / start_val * 100
all_results.append({
'start': current_start.date(),
'end': current_end.date(),
'strategy_return_pct': strategy_ret,
'benchmark_return_pct': benchmark_ret,
'final_value': final_val,
})
print(f"Strategy Return: {strategy_ret:.2f}% | Buy & Hold Return: {benchmark_ret:.2f}%")
# Advance start date for the next window
current_start += rd.relativedelta(months=window_months)
# Break if the next window's start date goes beyond the overall end date
if current_start > end_dt:
break
return pd.DataFrame(all_results)ticker, start,
end: The asset symbol and the overall historical
period to cover.window_months: The length of each
individual backtesting segment in months.strategy_class: The
backtrader.Strategy class to be tested (e.g.,
AOSaucerStrategy).strategy_params: A dictionary to pass
specific parameters to the strategy for each run.yfinance with
auto_adjust=False and droplevel applied.backtrader.Cerebro instance.strategy_class with its
parameters.Pandas DataFrame containing the
results for each rolling window, which can then be analyzed to
understand the strategy’s performance consistency. The AOSaucerStrategy offers a structured approach to
trend-continuation trading using Bill Williams’ Awesome Oscillator
patterns. By combining the “Saucer” entry pattern with
a long-term EMA trend filter and a dynamic ATR
trailing stop, it aims to capture momentum shifts within
established trends while managing risk effectively. The use of a
rolling backtest provides a more robust and realistic
assessment of its potential performance across various market cycles. As
always, thorough analysis of backtesting results and further
optimization are recommended before considering any strategy for live
trading.