← Back to Home
Volatility Oscillator Divergence Trading Strategy with ATR-Based Risk Management

Volatility Oscillator Divergence Trading Strategy with ATR-Based Risk Management

This article describes a trading strategy implemented in Backtrader that identifies divergence between price movements and volatility oscillator changes to generate trade signals. The strategy combines trend confirmation, volatility-adjusted position sizing, and ATR-based trailing stops to capitalize on potential reversals or continuations in trending markets.

Strategy Overview

The Volatility Oscillator Divergence Trading Strategy includes the following components:

Code Implementation

Below is the complete Backtrader code for the strategy:

import backtrader as bt
import numpy as np

class VolatilityOscillatorDivergenceStrategy(bt.Strategy):
    params = (
        ('vol_window', 20),
        ('vol_roc_period', 5),
        ('price_lookback', 5),
        ('sma_period', 30),
        ('atr_period', 14),
        ('atr_multiplier', 3.0),
        ('rsi_period', 14),
        ('max_hold_bars', 30),
        ('min_gap_bars', 1),
    )
    
    def __init__(self):
        self.returns = bt.indicators.PctChange(period=1)
        self.volatility = bt.indicators.StdDev(self.returns, period=self.params.vol_window)
        self.vol_osc = bt.indicators.ROC(self.volatility, period=self.params.vol_roc_period)
        self.sma = bt.indicators.SMA(period=self.params.sma_period)
        self.atr = bt.indicators.ATR(period=self.params.atr_period)
        self.rsi = bt.indicators.RSI(period=self.params.rsi_period)
        
        self.entry_bar = None
        self.last_trade_bar = None
        self.stop_price = None
        
    def next(self):
        if len(self.data) < 50:
            return
            
        current_price = self.data.close[0]
        
        # Exit on hold limit
        if self.position and self.entry_bar:
            if (len(self.data) - self.entry_bar) >= self.params.max_hold_bars:
                self.close()
                self.entry_bar = None
                self.last_trade_bar = len(self.data)
                return
        
        # Exit on trailing stop
        if self.position and self.stop_price:
            if self.position.size > 0 and current_price <= self.stop_price:
                self.close()
                self.entry_bar = None
                self.last_trade_bar = len(self.data)
                return
            elif self.position.size < 0 and current_price >= self.stop_price:
                self.close()
                self.entry_bar = None
                self.last_trade_bar = len(self.data)
                return
        
        # Update trailing stop
        if self.position:
            if self.position.size > 0:
                new_stop = current_price - self.params.atr_multiplier * self.atr[0]
                if self.stop_price is None or new_stop > self.stop_price:
                    self.stop_price = new_stop
            else:
                new_stop = current_price + self.params.atr_multiplier * self.atr[0]
                if self.stop_price is None or new_stop < self.stop_price:
                    self.stop_price = new_stop
        
        # Check minimum gap
        if self.last_trade_bar and (len(self.data) - self.last_trade_bar) < self.params.min_gap_bars:
            return
        
        # Entry signals
        if not self.position:
            price_change = self.data.close[0] - self.data.close[-self.params.price_lookback]
            vol_osc_change = self.vol_osc[0] - self.vol_osc[-self.params.price_lookback]
            
            # Volatility-adjusted position size
            atr_pct = self.atr[0] / current_price
            size_factor = min(0.8, max(0.2, 0.1 / atr_pct)) if atr_pct > 0 else 0.5
            cash = self.broker.getcash()
            size = int(cash * size_factor / current_price)
            
            # Bullish divergence: price up + vol osc down + uptrend + RSI not overbought
            if (price_change > 0 and vol_osc_change < 0 and 
                current_price > self.sma[0] and self.rsi[0] < 70 and size > 0):
                self.buy(size=size)
                self.stop_price = current_price - self.params.atr_multiplier * self.atr[0]
                self.entry_bar = len(self.data)
                self.last_trade_bar = len(self.data)
                
            # Bearish divergence: price down + vol osc up + downtrend + RSI not oversold
            elif (price_change < 0 and vol_osc_change > 0 and 
                  current_price < self.sma[0] and self.rsi[0] > 30 and size > 0):
                self.sell(size=size)
                self.stop_price = current_price + self.params.atr_multiplier * self.atr[0]
                self.entry_bar = len(self.data)
                self.last_trade_bar = len(self.data)

Strategy Explanation

1. VolatilityOscillatorDivergenceStrategy

The strategy identifies divergences between price and volatility oscillator movements to generate trade signals, with risk management and trend confirmation:

Key Features

Pasted image 20250716051700.png Pasted image 20250716051717.png

Potential Improvements

This strategy is designed for markets with periodic divergences between price and volatility, such as stocks or cryptocurrencies, and can be backtested to evaluate performance across various timeframes and assets.