Skip to content
All library documents

Buying Large Pullbacks Within an Established Bull Trend

Article Strategy library · Author: Farrell Aultman (pinkfish)

Summary

The AC countertrend strategy buys sharp pullbacks while a broader trend filter remains bullish. It defines that regime using a faster 40-day EMA above an 80-day EMA. Pullback depth is measured as the gap between the close and the highest close over 20 days, scaled by the 40-day standard deviation of daily percentage changes. When the pullback reaches a configurable threshold, the strategy enters only if it is flat and commits full buying power.

A long position can exit when the EMA trend turns bearish, price closes above a configurable simple moving average, a stop is breached, the holding period expires, or the backtest ends. Defaults include a three-standard-deviation entry threshold, a 15% stop, and a 20-day maximum holding period. The document supplies executable rules and a SPY example, but no resulting performance statistics, benchmark, or cost assumptions. Investing all available buying power concentrates exposure, and the pullback measure and exit conditions may behave differently across assets and market regimes.

Key ideas

  • The strategy buys deep pullbacks only while the faster EMA remains above the slower EMA.
  • Pullback size is normalized by the rolling standard deviation of daily percentage returns.
  • Entries use full buying power and are allowed only when no position is open.
  • Exits include trend reversal, a moving-average condition, a stop, a time limit, or the final test bar.
  • The supplied SPY example describes a backtest but reports no performance evidence.

Tags

Full text
# ac-countertrend


# ac-countertrend









AC countertrend strategy.

Buy pullbacks in a bull market; go all-in on each entry.

Rules
-----
Bull market: 40-day EMA > 80-day EMA.

Pullback (standard deviations below the 20-day high close):

    pullback = (highest_close_20 - close) / daily_std_40

where ``daily_std_40`` is the 40-day standard deviation of daily price
changes (percent returns).

Entry (flat only):
    - Bull market
    - Pullback >= ``min_pullback`` standard deviations
    - Full buying power (all-in)

Exit (when long):
    - Bear market (40-day EMA <= 80-day EMA) when ``exit_on_bear`` is True, or
    - Close above ``exit_ma``-day simple moving average, or
    - Close below stop loss (``stop_loss_pct`` below entry, default 15%), or
    - Position held for ``hold_period`` trading days, or
    - Last bar of the backtest

## Source (MIT)

```python
"""
AC countertrend strategy.

Buy pullbacks in a bull market; go all-in on each entry.

Rules
-----
Bull market: 40-day EMA > 80-day EMA.

Pullback (standard deviations below the 20-day high close):

    pullback = (highest_close_20 - close) / daily_std_40

where ``daily_std_40`` is the 40-day standard deviation of daily price
changes (percent returns).

Entry (flat only):
    - Bull market
    - Pullback >= ``min_pullback`` standard deviations
    - Full buying power (all-in)

Exit (when long):
    - Bear market (40-day EMA <= 80-day EMA) when ``exit_on_bear`` is True, or
    - Close above ``exit_ma``-day simple moving average, or
    - Close below stop loss (``stop_loss_pct`` below entry, default 15%), or
    - Position held for ``hold_period`` trading days, or
    - Last bar of the backtest
"""

import datetime

import pinkfish as pf


pf.DEBUG = False

default_options = {
    'use_adj': False,
    'use_cache': True,
    'margin': 1,
    'std_period': 40,
    'high_period': 20,
    'ema_fast': 40,
    'ema_slow': 80,
    'hold_period': 20,
    'min_pullback': 3.0,
    'exit_ma': 20,
    'stop_loss_pct': 0.15,
    'exit_on_bear': True,
}


class Strategy:

    def __init__(self, symbol, capital, start, end, options=default_options):

        self.symbol = symbol
        self.capital = capital
        self.start = start
        self.end = end
        self.options = options.copy()

        self.ts = None
        self.tlog = None
        self.dbal = None
        self.stats = None
        self.bars_held = 0

    def _algo(self):

        pf.TradeLog.cash = self.capital
        pf.TradeLog.margin = self.options['margin']
        hold_period = self.options['hold_period']
        min_pullback = self.options['min_pullback']
        stop_loss_pct = self.options.get('stop_loss_pct', 0.15)
        exit_on_bear = self.options.get('exit_on_bear', True)
        stop_loss = 0

        for i, row in enumerate(self.ts.itertuples()):

            date = row.Index.to_pydatetime()
            close = row.close
            end_flag = pf.is_last_row(self.ts, i)

            if self.tlog.shares > 0:
                self.bars_held += 1
                bear_exit = exit_on_bear and not row.bull
                above_ma = close > row.exit_ma
                hit_stop = stop_loss > 0 and close < stop_loss
                if (bear_exit
                        or above_ma
                        or hit_stop
                        or self.bars_held >= hold_period
                        or end_flag):
                    self.tlog.sell(date, close)
                    self.bars_held = 0
                    stop_loss = 0

            else:
                self.bars_held = 0
                if (row.bull
                        and row.pullback >= min_pullback
                        and row.daily_std > 0):
                    self.tlog.buy(date, close)
                    self.bars_held = 1
                    if stop_loss_pct < 1:
                        stop_loss = (1 - stop_loss_pct) * close

            self.dbal.append(date, close)

    def run(self):

        opts = self.options
        self.ts = pf.fetch_timeseries(self.symbol, use_cache=opts['use_cache'])
        self.ts = pf.select_tradeperiod(
            self.ts, self.start, self.end, use_adj=opts['use_adj'])

        self.ts['ema_fast'] = pf.EMA(self.ts, timeperiod=opts['ema_fast'])
        self.ts['ema_slow'] = pf.EMA(self.ts, timeperiod=opts['ema_slow'])
        self.ts['bull'] = self.ts['ema_fast'] > self.ts['ema_slow']

        self.ts['daily_std'] = (
            self.ts['close'].pct_change().rolling(opts['std_period']).std())

        self.ts['high_close'] = (
            self.ts['close'].rolling(opts['high_period']).max())

        self.ts['pullback'] = (
            (self.ts['high_close'] - self.ts['close']) / self.ts['daily_std'])

        self.ts['exit_ma'] = pf.SMA(self.ts, timeperiod=opts['exit_ma'])

        self.ts, self.start = pf.finalize_timeseries(
            self.ts, self.start, dropna=True,
            drop_columns=['open', 'high', 'low'])

        self.tlog = pf.TradeLog(self.symbol)
        self.dbal = pf.DailyBal()

        self._algo()
        self._get_logs()
        self._get_stats()

    def _get_logs(self):
        self.tlog = self.tlog.get_log()
        self.dbal = self.dbal.get_log(self.tlog)

    def _get_stats(self):
        self.stats = pf.stats(self.ts, self.tlog, self.dbal, self.capital)


def main():
    """Run a backtest with the same defaults as strategy.ipynb."""
    symbol = 'SPY'
    capital = 10000
    start = datetime.datetime(*pf.SP500_BEGIN)
    end = datetime.datetime.now()
    s = Strategy(symbol, capital, start, end)
    s.run()
    pf.print_full(s.stats)


if __name__ == '__main__':
    main()

```

Shown in full with attribution under the source's licence. Licence: MIT

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.