Docs
Open Studio

US examples

AAPL Bollinger rebound

On this page5

Buying the dip is a classic contrarian strategy; this page tests it quantitatively on Apple (AAPL) with Bollinger Bands. It suits patient investors who want a single-stock, mean-reversion trade that waits for statistically extreme drops.

Bollinger Bands compute a moving average and standard deviation over a rolling window of d days, and draw an upper and a lower band n standard deviations from the mean. Most price action stays inside the bands, so a move beyond them signals a possible mean reversion.

  • A larger d gives a smoother price envelope that filters out short-term noise.
  • A larger n gives more extreme, and rarer, signals. Following the 68-95-99.7 rule, n is usually between 1 and 3. At n = 2, roughly 95% of prices fall inside the bands under a normal distribution.

A close below the lower band may be an oversold signal. The hypothesis is that the price then reverts and bounces back toward the middle band.

Strategy logic

  • Bollinger Bands use a 40-day moving average and 2.5 standard deviations.
  • Buy AAPL when the closing price falls below the lower band.
  • Sell when the price recovers above the middle band.
  • Track each trade's best and worst moves (MAE/MFE) over 40 days after entry.

Code

strategy.py
from finlab import data
from finlab.backtest import sim
from finlab.markets.us import USMarket

data.set_market('us')

close = data.get('price:adj_close')
close = close[close.index.dayofweek < 5]  # remove weekends

upperband, middleband, lowerband = data.indicator(
    'BBANDS', adjust_price=True, resample='D', nbdevup=2.5, nbdevdn=2.5, timeperiod=40
)
entries = lowerband > close
exits = close > middleband

position = entries.hold_until(exits)['AAPL']
report = sim(
    position, market=USMarket(), fee_ratio=0.001, tax_ratio=0, mae_mfe_window=40
)
report.display()

Read the results

report.display() draws the cumulative return chart. Two more views help with a contrarian strategy.

MAE (Maximum Adverse Excursion) and MFE (Maximum Favorable Excursion) measure the worst drawdown and the best run-up during each trade's holding period, so you can see the strategy's risk and reward trade by trade:

report.display_mae_mfe_analysis(violinmode='overlay').show()

The trade records list every trade:

trade_record = report.trades
trade_record

How to adjust it

  • adjust_price=True: computes the bands from adjusted prices, the same scale as price:adj_close. The default (False) uses unadjusted prices, so after AAPL's stock splits the bands would sit far above the adjusted close.
  • nbdevup=2.5 / nbdevdn=2.5: 2.5 standard deviations for the upper and lower bands, which makes entry signals relatively rare and extreme.
  • timeperiod=40: a 40-day window for the moving average and standard deviation, balancing smoothness with responsiveness.
  • mae_mfe_window=40: tracks MAE/MFE over 40 days after each entry, a detailed picture of per-trade risk.
  • fee_ratio=0.001: a 0.1% commission, a reasonable estimate for US brokerage fees.
  • tax_ratio=0: US stock trades carry no transaction tax.

Add a stop-loss and a take-profit

Catching a falling knife is risky: whether or not you catch it, be ready to let go quickly. In extreme sell-offs this contrarian approach can achieve a high win rate with short holding periods, but explicit exit rules can significantly improve risk management:

  • If the position is profitable during the first 10 holding days, the dip was likely caught near the bottom, and mean reversion is playing out as expected.
  • If it is underwater during the first 10 days, the bottom may not have been reached yet, and further downside is possible.
  • If the loss exceeds 15%, it may be a rare, out-of-sample crash, and the position should be cut immediately.

This version adds a 10% take-profit (take_profit=0.1, sell at a 10% gain from the entry price) and a 15% stop-loss (stop_loss=-0.15, sell at a 15% loss). Change the hold_until line and run the MAE/MFE chart with the report; everything else stays the same:

position = entries.hold_until(exits, take_profit=0.1, stop_loss=-0.15)['AAPL']
report = sim(
    position, market=USMarket(), fee_ratio=0.001, tax_ratio=0, mae_mfe_window=40
)
report.display_mae_mfe_analysis(violinmode='overlay').show()
report.display()

The stops reduce tail risk, at the cost of possibly capping some winning trades. The MAE/MFE analysis is especially useful for calibrating these stop-loss and take-profit levels from historical trades.

↑↓ Move Enter Open Esc Close