This article explores a trading strategy built using Backtrader, combining the Variable Index Dynamic Average (VIDYA) with the Chande Momentum Oscillator (CMO), Average Directional Index (ADX), and momentum filters. The strategy aims to adapt to market conditions by dynamically adjusting the VIDYA period based on momentum, incorporating trend strength and momentum validation to filter trades, and using trailing stops for risk management.
The VIDYA strategy leverages the following components:
Below is the complete Backtrader code for the strategy:
import backtrader as bt
import numpy as np
class CMOIndicator(bt.Indicator):
= ('cmo',)
lines = (('period', 14),)
params
def __init__(self):
self.addminperiod(self.params.period)
def next(self):
= losses = 0.0
gains for i in range(1, self.params.period + 1):
if len(self.data) > i:
= self.data.close[-i] - self.data.close[-i-1]
change if change > 0:
+= change
gains else:
+= abs(change)
losses
self.lines.cmo[0] = 100 * (gains - losses) / (gains + losses) if gains + losses > 0 else 0
class VIDYAStrategy(bt.Strategy):
= (
params 'cmo_period', 14),
('period_min', 10),
('period_max', 60),
('atr_period', 14),
('atr_multiplier', 1.5),
('cooldown_bars', 3),
('threshold_pct', 0.01),
('adx_period', 14),
('adx_threshold', 20),
('momentum_period', 30),
('momentum_threshold', 0.01),
(
)
def __init__(self):
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)
self.momentum = bt.indicators.Momentum(period=self.params.momentum_period)
self.vidya_value = None
self.prev_vidya_value = None
self.last_exit_bar = 0
self.order = None
def next(self):
= max(self.params.cmo_period, self.params.period_max, self.params.atr_period,
min_periods self.params.adx_period, self.params.momentum_period)
if len(self.data) < min_periods + 1:
return
# Cancel pending orders
if self.order:
return
# Store previous VIDYA before update
self.prev_vidya_value = self.vidya_value
# Calculate adaptive period using lagged CMO
= min(1.0, abs(self.cmo[-1]) / 100.0)
lagged_norm_abs_cmo = self.params.period_max - lagged_norm_abs_cmo * (self.params.period_max - self.params.period_min)
adaptive_period = 2.0 / (adaptive_period + 1)
alpha
# Initialize/update VIDYA
if self.vidya_value is None:
self.vidya_value = self.data.close[0]
return
self.vidya_value = alpha * self.data.close[0] + (1 - alpha) * self.vidya_value
# Cooldown check
if (len(self.data) - self.last_exit_bar) < self.params.cooldown_bars:
return
# TREND STRENGTH FILTER - ADX must be above threshold
if self.adx[0] < self.params.adx_threshold:
return
# MOMENTUM VALIDATOR - Recent momentum must be strong enough
= (self.momentum[0] / self.data.close[-self.params.momentum_period]) * 100
momentum_pct if abs(momentum_pct) < self.params.momentum_threshold:
return
# Entry with threshold confirmation + filters
if not self.position:
= lagged_vidya * self.params.threshold_pct
threshold
# Long: price above VIDYA + positive momentum + strong trend
if (lagged_close > (lagged_vidya + threshold) and
> self.params.momentum_threshold):
momentum_pct self.order = self.buy()
# Set trailing stop
self.sell(exectype=bt.Order.StopTrail, trailamount=self.params.atr_multiplier * self.atr[0])
# Short: price below VIDYA + negative momentum + strong trend
elif (lagged_close < (lagged_vidya - threshold) and
< -self.params.momentum_threshold):
momentum_pct self.order = self.sell()
# Set trailing stop
self.buy(exectype=bt.Order.StopTrail, trailamount=self.params.atr_multiplier * self.atr[0])
# Exit on signal reversal
elif self.position.size > 0 and lagged_close < lagged_vidya:
self.close()
self.last_exit_bar = len(self.data)
elif self.position.size < 0 and lagged_close > lagged_vidya:
self.close()
self.last_exit_bar = len(self.data)
def notify_order(self, order):
if order.status in [order.Completed, order.Canceled, order.Margin, order.Rejected]:
self.order = None
The CMOIndicator
calculates the Chande Momentum
Oscillator, which compares the sum of price gains to losses over a
specified period (default: 14). The formula is:
\[ \text{CMO} = 100 \times \frac{\text{Gains} - \text{Losses}}{\text{Gains} + \text{Losses}} \]
This produces a value between -100 and 100, reflecting momentum strength. High absolute CMO values indicate strong trends, used to adjust VIDYA’s responsiveness.
The strategy initializes indicators (CMO, ATR, ADX, Momentum) and
tracks VIDYA values. Key logic in the next
method
includes:
\[\text{Adaptive Period} = \text{period}_{\text{max}} - |\text{CMO}| \times \left( \text{period}_{\text{max}} - \text{period}_{\text{min}} \right) \]
\[ \alpha = \frac{2}{\text{Adaptive Period} + 1} \]
\[ \text{VIDYA}_t = \alpha \times \text{Close}_t + (1 - \alpha) \times \text{VIDYA}_{t-1} \]
This strategy is designed for trending markets and can be backtested with historical data to evaluate performance across different assets and timeframes.