The Volatility Index Dynamic Average (VIDYA), created by Tushar
Chande, is an adaptive moving average that adjusts its smoothing period
based on market volatility. This makes it more responsive during
trending periods and smoother during choppy or range-bound markets. This
article explores a backtrader implementation of the VIDYA
indicator, coupled with the Chande Momentum Oscillator (CMO), Average
Directional Index (ADX), and a price momentum filter to create a robust
trend-following strategy, explicitly incorporating your preferred use of
trailing stops for risk management.
At the heart of the strategy is the VIDYA indicator. Unlike simple moving averages with fixed periods, VIDYA dynamically changes its period based on a “Chande Momentum Oscillator (CMO) efficiency ratio”.
Chande Momentum Oscillator (CMO): This
oscillator measures momentum by comparing the sum of recent gains to the
sum of recent losses over a specified period. The formula is: \[CMO = \frac{Sum(Gains) - Sum(Losses)}{Sum(Gains)
+ Sum(Losses)} \times 100\] It ranges from -100 to +100. The
CMOIndicator class in the provided code calculates this
value.
Adaptive Period Calculation: The absolute value
of the CMO, normalized to a 0-1 range, acts as an efficiency ratio. When
abs(CMO) is high (approaching 100), it indicates strong
momentum and low noise, leading to a shorter
adaptive_period for VIDYA. Conversely, when
abs(CMO) is low (approaching 0), it suggests low momentum
and higher noise, resulting in a longer adaptive_period.
This adaptive period is then used to calculate the alpha
(smoothing factor) for the exponential moving average-like calculation
of VIDYA: \[Alpha = \frac{2}{AdaptivePeriod +
1}\] \[VIDYA_t = Alpha \times Close_t
+ (1 - Alpha) \times VIDYA_{t-1}\]
This dynamic adjustment allows VIDYA to quickly react to new trends while filtering out noise during consolidation phases.
VIDYACMOADXMomentumStrategy:
The Confirmed Trend FollowerThis strategy, named VIDYACMOADXMomentumStrategy to
reflect its components, combines the adaptive nature of VIDYA with
several confirmation filters to enhance signal quality.
import backtrader as bt
import numpy as np
class CMOIndicator(bt.Indicator):
lines = ('cmo',)
params = (('period', 14),)
def __init__(self):
self.addminperiod(self.params.period)
def next(self):
gains = losses = 0.0
# Calculate sums of gains and losses over the period
# Note: self.data.close[-i] refers to 'i' bars ago, self.data.close[-i-1] is 'i+1' bars ago
for i in range(1, self.params.period + 1):
# Ensure enough historical data exists
if len(self.data) > i:
change = self.data.close[-i] - self.data.close[-i-1]
if change > 0:
gains += change
else:
losses += abs(change)
# Calculate CMO. Handle division by zero.
denominator = gains + losses
self.lines.cmo[0] = 100 * (gains - losses) / denominator if denominator > 0 else 0
class VIDYACMOADXMomentumStrategy(bt.Strategy):
params = (
('cmo_period', 10), # Period for CMO calculation
('period_min', 10), # Minimum VIDYA smoothing period
('period_max', 60), # Maximum VIDYA smoothing period
('atr_period', 14), # Period for Average True Range
('atr_multiplier', 2.), # Multiplier for ATR-based trailing stop
('cooldown_bars', 3), # Bars to wait after an exit before re-entry
('threshold_pct', 0.015), # Percentage threshold for price crossing VIDYA
('adx_period', 14), # Period for ADX
('adx_threshold', 20), # ADX threshold for trend strength
('momentum_period', 50), # Period for Momentum indicator
('momentum_threshold', 0.005), # Minimum required momentum percentage
)
def __init__(self):
# Initialize required indicators
self.cmo = CMOIndicator(period=self.params.cmo_period)
self.atr = bt.indicators.ATR(period=self.params.atr_period)
self.adx = bt.indicators.AverageDirectionalMovementIndex(period=self.params.adx_period)
# Momentum calculates price change over 'period' bars
self.momentum = bt.indicators.Momentum(self.data.close, period=self.params.momentum_period)
self.vidya_value = None # Current VIDYA value
self.prev_vidya_value = None # Previous VIDYA value
self.last_exit_bar = 0 # To track cooldown period
self.order = None # To track pending orders
self.trailing_stop_order = None # To track the active trailing stop order
def next(self):
# Ensure sufficient data for all indicators
min_periods = max(self.params.cmo_period, self.params.period_max, self.params.atr_period,
self.params.adx_period, self.params.momentum_period)
if len(self.data) < min_periods + 1:
return
# If an order is pending, do not generate new signals
if self.order:
return
# Store previous VIDYA value before calculating the current one
self.prev_vidya_value = self.vidya_value
# Calculate adaptive period using lagged (previous bar's) CMO
# CMO value is between -100 and 100, so abs(CMO)/100 normalizes it to 0-1
lagged_norm_abs_cmo = min(1.0, abs(self.cmo[-1]) / 100.0)
# Adaptive period ranges from period_max (low momentum) to period_min (high momentum)
adaptive_period = self.params.period_max - lagged_norm_abs_cmo * (self.params.period_max - self.params.period_min)
# Calculate smoothing factor alpha
alpha = 2.0 / (adaptive_period + 1)
# Initialize VIDYA on first sufficient bar
if self.vidya_value is None:
self.vidya_value = self.data.close[0]
return
# Update VIDYA
self.vidya_value = alpha * self.data.close[0] + (1 - alpha) * self.vidya_value
# Define lagged values for comparison (signals are based on completed bars)
lagged_close = self.data.close[-1]
lagged_vidya = self.prev_vidya_value
# --- Entry Filters ---
# Cooldown period after an exit
if (len(self.data) - self.last_exit_bar) < self.params.cooldown_bars:
return
# TREND STRENGTH FILTER - ADX must be above threshold (strong trend)
if self.adx[0] < self.params.adx_threshold:
return
# MOMENTUM VALIDATOR - Recent momentum must be strong enough
# Momentum indicator returns the difference between current and 'period' bars ago
# Convert to percentage change for threshold comparison
# Ensure there's enough data for momentum calculation
if len(self.data) <= self.params.momentum_period:
return # Not enough data for momentum
# Use previous close as reference for percentage change, safer than 0 if momentum[0] is price diff
if self.data.close[-self.params.momentum_period] == 0:
return # Avoid division by zero
momentum_pct = (self.momentum[0] / self.data.close[-self.params.momentum_period])
# Note: momentum_threshold is typically a decimal (e.g., 0.005 for 0.5%)
# Check for NaN values in indicators before proceeding
if np.isnan(lagged_vidya) or np.isnan(self.adx[0]) or np.isnan(momentum_pct):
return
# --- Entry Logic ---
if not self.position: # Only consider new entries if not in a position
threshold = lagged_vidya * self.params.threshold_pct
# Long Entry: Current close crosses above VIDYA by a threshold, with positive momentum and strong trend
if (lagged_close > (lagged_vidya + threshold) and
momentum_pct > self.params.momentum_threshold):
self.order = self.buy()
# Short Entry: Current close crosses below VIDYA by a threshold, with negative momentum and strong trend
elif (lagged_close < (lagged_vidya - threshold) and
momentum_pct < -self.params.momentum_threshold):
self.order = self.sell()
# --- Exit Logic (Signal Reversal) ---
# If long and price closes below VIDYA
elif self.position.size > 0 and lagged_close < lagged_vidya:
self.close()
self.last_exit_bar = len(self.data)
if self.trailing_stop_order: # Cancel existing trailing stop
self.cancel(self.trailing_stop_order)
self.trailing_stop_order = None # Clear the reference
# If short and price closes above VIDYA
elif self.position.size < 0 and lagged_close > lagged_vidya:
self.close()
self.last_exit_bar = len(self.data)
if self.trailing_stop_order: # Cancel existing trailing stop
self.cancel(self.trailing_stop_order)
self.trailing_stop_order = None # Clear the reference
def notify_order(self, order):
if order.status in [order.Completed]:
# If an entry order is completed, place a trailing stop
if order.isbuy():
# The trailing stop will be a sell order
self.trailing_stop_order = self.sell(
exectype=bt.Order.StopTrail,
trailamount=self.params.atr_multiplier * self.atr[0]
)
elif order.issell(): # This means a short entry order completed
# The trailing stop will be a buy order
self.trailing_stop_order = self.buy(
exectype=bt.Order.StopTrail,
trailamount=self.params.atr_multiplier * self.atr[0]
)
# Reset the order tracking variable if the order is completed, canceled, or rejected
if order.status in [order.Completed, order.Canceled, order.Margin, order.Rejected]:
self.order = None # Clear the main order reference
if order == self.trailing_stop_order: # If the current order is the trailing stop
self.trailing_stop_order = None # Clear its reference tooInitialization (__init__):
CMOIndicator, ATR (Average
True Range), ADX (Average Directional Index), and
Momentum indicators.vidya_value,
prev_vidya_value, last_exit_bar (for
cooldown), and order references (self.order,
self.trailing_stop_order).next() Method (Per Bar Logic):
lagged_norm_abs_cmo from the previous bar’s
CMO, which drives the adaptiveness.adaptive_period for VIDYA, which shrinks
with strong momentum (high abs(CMO)) and expands with low
momentum (low abs(CMO)).alpha smoothing factor.self.vidya_value using the current close price
and the adaptive alpha.cooldown_bars).adx_threshold to ensure a sufficiently strong trend
is present. This helps avoid choppy markets.momentum_period) meets a
momentum_threshold. This ensures that entries are only made
when there’s significant directional conviction.vidya_value by a
threshold_pct.momentum_pct and the
ADX filter, it triggers buy() or sell()
orders.vidya_value, the position is closed.vidya_value, the position is closed.last_exit_bar is updated, and any
active trailing stop order for that position is canceled.notify_order() Method (Order
Lifecycle):
buy or sell entry order completes, a
corresponding trailing stop order is immediately placed.
sell trailing stop is placed
with a trailamount based on a multiple of the current
ATR.buy trailing stop is placed
with the same ATR-based trailamount.self.order
and self.trailing_stop_order flags once orders are
completed, canceled, or rejected, allowing the strategy to process new
signals.This VIDYACMOADXMomentumStrategy represents a
well-structured approach to trend following, using an adaptive moving
average as its core, reinforced by multiple indicators to confirm trend
strength and momentum, and safeguarded by the consistent application of
trailing stops.