← Back to Home
Pullback–Resume Dynamics with Williams %R, MACD Histogram Confirmation, and ATR-Trailed Risk

Pullback–Resume Dynamics with Williams %R, MACD Histogram Confirmation, and ATR-Trailed Risk

A directional pullback framework that:

  1. defines regime with an exponential trend filter;
  2. detects pullbacks with Williams %R crosses out of extremes;
  3. requires MACD histogram to confirm momentum realignment; and
  4. delegates exits to an ATR-multiple trailing stop that ratchets only in the trade’s favor.

The approach targets mean-reverting pauses within trends rather than countertrend fades.

Exponential Moving Average (EMA)

For close \(C_t\) and period \(n\), smoothing factor \(\alpha=\frac{2}{n+1}\):

\[ \text{EMA}_t=\alpha C_t+(1-\alpha)\text{EMA}_{t-1} \]

Regime proxy: uptrend if \(C_t>\text{EMA}_t\); downtrend if \(C_t<\text{EMA}_t\).

Williams %R

For lookback \(L\) with highest high \(H_t^{(L)}\) and lowest low \(L_t^{(L)}\):

\[ \%R_t=-100\cdot \frac{H_t^{(L)}-C_t}{H_t^{(L)}-L_t^{(L)}} \]

Scale \([-100,0]\). Oversold below threshold (e.g., \(-70\)), overbought above threshold (e.g., \(-30\)).

Crossing logic:

MACD Histogram

With fast EMA \(\text{EMA}^{(f)}\), slow EMA \(\text{EMA}^{(s)}\), and signal EMA of MACD:

\[ \text{MACD}_t=\text{EMA}^{(f)}_t(C)-\text{EMA}^{(s)}_t(C),\quad \text{Signal}_t=\text{EMA}^{(\ell)}_t(\text{MACD}),\quad \text{Hist}_t=\text{MACD}_t-\text{Signal}_t \]

Momentum confirmation: long only if \(\text{Hist}_t>0\); short only if \(\text{Hist}_t<0\).

Average True Range (ATR) and trailing stop

True range:

\[ \text{TR}_t=\max\{H_t-L_t, |H_t-C_{t-1}|, |L_t-C_{t-1}|\} \]

ATR is an EMA of TR over \(n\) bars. Volatility stop:

Entry/exit protocol

Long setup

  1. Regime: \(C_t>\text{EMA}_t^{(trend)}\).
  2. Pullback release: \(\%R_t\) crosses upward through \(\theta_{\text{OS}}\) (e.g., \(-70\)).
  3. Momentum alignment: \(\text{Hist}_t>0\).
  4. Action: enter long at market next bar; initialize trailing stop at \(H^{\ast}_t-k\cdot \text{ATR}_t\).

Short setup

  1. Regime: \(C_t<\text{EMA}_t^{(trend)}\).
  2. Pullback release: \(\%R_t\) crosses downward through \(\theta_{\text{OB}}\) (e.g., \(-30\)).
  3. Momentum alignment: \(\text{Hist}_t<0\).
  4. Action: enter short; initialize trailing stop at \(L^{\ast}_t+k\cdot \text{ATR}_t\).

Exit

Main strategy logic (Backtrader next loop only)

def next(self):
    if self.order:
        return

    if not self.position:
        # Regime via EMA trend
        uptrend   = self.data.close[0] > self.trend_ema[0]
        downtrend = self.data.close[0] < self.trend_ema[0]

        # Pullback-release with MACD histogram confirmation
        if uptrend and self.buy_signal[0]:          # %R crosses up from oversold
            if self.macd_histo[0] > 0:              # momentum aligned
                self.order = self.buy()

        elif downtrend and self.sell_signal[0]:     # %R crosses down from overbought
            if self.macd_histo[0] < 0:              # momentum aligned
                self.order = self.sell()

    else:
        # ATR-multiple trailing stop that only tightens
        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()

Parameter guidance

Microstructure and robustness notes

Expected behavior by regime

This construction formalizes a classic pullback–resume motif: a structural trend filter, an oscillator that detects release from extremes, a momentum check to avoid early entries, and an exit that is fully delegated to volatility geometry.

Here’s a detailed interpretation of your rolling backtest results:

Rolling Backtest Performance

Let’s try a rolling backtest to see the performance over time:


strategy = load_strategy("WilliamsPullbackStrategy")

def run_rolling_backtest(
    ticker,
    start,
    end,
    window_months,
    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)
        if current_end > end_dt:
            break

        print(f"\nROLLING BACKTEST: {current_start.date()} to {current_end.date()}")

        data = yf.download(ticker, start=current_start, end=current_end, progress=False)
        if data.empty or len(data) < 90:
            print("Not enough data.")
            current_start += rd.relativedelta(months=window_months)
            continue

        data = data.droplevel(1, 1) if isinstance(data.columns, pd.MultiIndex) else data

        feed = bt.feeds.PandasData(dataname=data)
        cerebro = bt.Cerebro()
        cerebro.addstrategy(strategy, **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()
        cerebro.run()
        final_val = cerebro.broker.getvalue()
        ret = (final_val - start_val) / start_val * 100

        all_results.append({
            'start': current_start.date(),
            'end': current_end.date(),
            'return_pct': ret,
            'final_value': final_val,
        })

        print(f"Return: {ret:.2f}% | Final Value: {final_val:.2f}")
        current_start += rd.relativedelta(months=window_months)

    return pd.DataFrame(all_results)
    
    
ticker = "SOL-USD"
start = "2018-01-01"
end = "2025-01-01"
window_months = 12

df = run_rolling_backtest(ticker=ticker, start=start, end=end, window_months=window_months)
Pasted image 20250815205450.png

1. Return distribution

2. Risk-adjusted metrics

3. Risk control

4. Regime sensitivity

Takeaways