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 ---
= self.data.close[0] > self.trend_ema[0]
is_uptrend = self.data.close[0] < self.trend_ema[0]
is_downtrend
# --- 2. Check for Saucer Patterns ---
# Bullish Saucer requires AO > 0
if is_uptrend and self.ao[0] > 0:
= self.ao[0] > self.ao[-1]
is_green_bar = self.ao[-1] < 0 and self.ao[-1] > self.ao[-2]
is_second_red_bar_smaller
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:
= self.ao[0] < self.ao[-1]
is_red_bar = self.ao[-1] > 0 and self.ao[-1] < self.ao[-2]
is_second_green_bar_smaller
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])
= self.highest_price_since_entry - (self.atr[0] * self.p.atr_stop_multiplier)
new_stop 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])
= self.lowest_price_since_entry + (self.atr[0] * self.p.atr_stop_multiplier)
new_stop 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,=None
strategy_params
):= strategy_params or {}
strategy_params = []
all_results = pd.to_datetime(start)
start_dt = pd.to_datetime(end)
end_dt = start_dt
current_start
while True:
= current_start + rd.relativedelta(months=window_months)
current_end # Ensure the current_end does not exceed the overall end date
if current_end > end_dt:
= end_dt # Adjust current_end to the overall end_dt if it overshoots
current_end 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
= yf.download(ticker, start=current_start, end=current_end, auto_adjust=False, progress=False)
data
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
+= rd.relativedelta(months=window_months)
current_start continue
if isinstance(data.columns, pd.MultiIndex):
= data.droplevel(1, 1)
data
# Calculate Buy & Hold return for the period
= data['Close'].iloc[0]
start_price = data['Close'].iloc[-1]
end_price = (end_price - start_price) / start_price * 100
benchmark_ret
= bt.feeds.PandasData(dataname=data)
feed = bt.Cerebro()
cerebro
**strategy_params)
cerebro.addstrategy(strategy_class,
cerebro.adddata(feed)100000)
cerebro.broker.setcash(=0.001)
cerebro.broker.setcommission(commission=95)
cerebro.addsizer(bt.sizers.PercentSizer, percents
= cerebro.broker.getvalue()
start_val try:
cerebro.run()except Exception as e:
print(f"Error running backtest for {current_start.date()} to {current_end.date()}: {e}")
+= rd.relativedelta(months=window_months)
current_start continue
= cerebro.broker.getvalue()
final_val = (final_val - start_val) / start_val * 100
strategy_ret
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
+= rd.relativedelta(months=window_months)
current_start
# 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.