← Back to Home
Capturing Market Moves with Volatility Momentum

Capturing Market Moves with Volatility Momentum

Volatility is often treated as a background variable — traders look at whether it’s high or low, then adjust risk. But what if volatility itself has momentum? What if accelerating volatility is an early warning signal that big price moves are coming?

This is the foundation of the SimpleVolatilityMomentumStrategy, a trading approach that looks for periods when volatility is expanding rapidly. When this condition aligns with the price trend, the strategy enters trades. An adaptive ATR-based trailing stop provides dynamic risk management, and the system is stress-tested using a rolling backtesting framework.

The Idea Behind the Strategy

The key concept is simple. Markets tend to make large moves when volatility itself begins to trend upward. If volatility is accelerating and the price is in an uptrend, go long. If volatility is accelerating and the price is in a downtrend, go short. Exit when volatility momentum dies out or when an adaptive stop-loss is triggered.

Core components

Entry conditions

Exit conditions

This ensures trades are only held during periods when volatility is actively expanding.

Implementation in Backtrader

The following code implements the strategy in Backtrader. Explanations are provided inline.

import backtrader as bt
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

class SimpleVolatilityMomentumStrategy(bt.Strategy):
    """Trade in the direction of price when volatility accelerates"""
    
    params = (
        ('vol_window', 30),
        ('vol_momentum_window', 7),
        ('price_sma_window', 30),
        ('atr_window', 14),
        ('atr_multiplier', 5.0),
    )
    
    def __init__(self):
        # Daily returns for volatility calculation
        self.returns = bt.indicators.PctChange(self.data.close, period=1)
        
        # Rolling volatility (std of returns)
        self.volatility = bt.indicators.StandardDeviation(self.returns, period=self.params.vol_window)
        
        # Volatility momentum = difference from N bars ago
        self.vol_momentum = self.volatility - self.volatility(-self.params.vol_momentum_window)
        
        # Price SMA for trend confirmation
        self.price_sma = bt.indicators.SMA(self.data.close, period=self.params.price_sma_window)
        
        # ATR for adaptive stop-loss
        self.atr = bt.indicators.ATR(self.data, period=self.params.atr_window)
        
        # Variables for trade tracking
        self.stop_price = 0
        self.trade_count = 0

    def log(self, txt, dt=None):
        dt = dt or self.datas[0].datetime.date(0)
        print(f'{dt.isoformat()}: {txt}')

    def notify_order(self, order):
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(f'LONG EXECUTED - Price: {order.executed.price:.2f}')
            elif order.issell():
                if self.position.size == 0:
                    self.log(f'POSITION CLOSED - Price: {order.executed.price:.2f}')
                else:
                    self.log(f'SHORT EXECUTED - Price: {order.executed.price:.2f}')

    def notify_trade(self, trade):
        if trade.isclosed:
            self.log(f'TRADE CLOSED - PnL: {trade.pnl:.2f}')
            self.stop_price = 0

    def next(self):
        # Make sure indicators have enough history
        min_bars_needed = max(self.params.vol_window, 
                              self.params.price_sma_window, 
                              self.params.atr_window) + self.params.vol_momentum_window
        if len(self) < min_bars_needed:
            return

        vol_momentum = self.vol_momentum[0]
        current_price = self.data.close[0]
        sma_price = self.price_sma[0]
        current_atr = self.atr[0]
        
        # Stop-loss checks
        if self.position:
            if self.position.size > 0 and current_price <= self.stop_price:
                self.close()
                self.log(f'STOP LOSS HIT (Long) at {current_price:.2f}')
                return
            elif self.position.size < 0 and current_price >= self.stop_price:
                self.close()
                self.log(f'STOP LOSS HIT (Short) at {current_price:.2f}')
                return
            
            # Trailing stop updates
            if self.position.size > 0:
                new_stop = current_price - current_atr * self.params.atr_multiplier
                if new_stop > self.stop_price:
                    self.stop_price = new_stop
            elif self.position.size < 0:
                new_stop = current_price + current_atr * self.params.atr_multiplier
                if new_stop < self.stop_price:
                    self.stop_price = new_stop
        
        # Exit when volatility momentum turns negative
        if self.position and vol_momentum <= 0:
            self.close()
            self.log(f'VOL MOMENTUM EXIT at {current_price:.2f}')
            return
        
        # Entry logic
        if not self.position and vol_momentum > 0:
            if current_price > sma_price:
                self.buy()
                self.stop_price = current_price - current_atr * self.params.atr_multiplier
                self.trade_count += 1
                self.log(f'LONG ENTRY at {current_price:.2f}, Stop {self.stop_price:.2f}')
            elif current_price < sma_price:
                self.sell()
                self.stop_price = current_price + current_atr * self.params.atr_multiplier
                self.trade_count += 1
                self.log(f'SHORT ENTRY at {current_price:.2f}, Stop {self.stop_price:.2f}')

    def stop(self):
        print(f'\n=== RESULTS ===')
        print(f'Total Trades: {self.trade_count}')
        print(f'Params: Vol={self.params.vol_window}, Mom={self.params.vol_momentum_window}, SMA={self.params.price_sma_window}, ATR={self.params.atr_window}×{self.params.atr_multiplier}')

Backtesting Framework

A single backtest isn’t enough. Markets change character, so we use rolling windows to test robustness. Each rolling period is evaluated separately.

from backtest import *

strategy = load_strategy_from_package('SimpleVolatilityMomentumStrategy')

symbol = "ETH/USDC"

df = fetch_binance_ohlcv_range(
    symbol=symbol,
    timeframe="1d",
    start="2025-01-01",
    )

collected = run_backtest_and_collect(strategy, df)

import matplotlib
matplotlib.use('inline')
plot_live(df, collected, title=symbol, instant_plot=True, log_mode="batch", frame_stride=1, w = 12, h = 8)

## Takeaways

The SimpleVolatilityMomentumStrategy turns volatility into a predictive tool instead of just a risk measure. By focusing on the acceleration of volatility, it identifies when markets are entering turbulent phases that often precede large price swings.

Strengths

Limitations

Next steps

Volatility doesn’t just describe markets. It can signal where they’re going. By trading volatility momentum, we can catch the tremors before the earthquake.