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 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
Volatility is measured as the rolling standard deviation of
returns over a vol_window.
Volatility momentum is defined as today’s volatility minus
volatility from vol_momentum_window bars ago.
A price SMA provides trend direction confirmation.
An ATR-based trailing stop adapts to changing market conditions, tightening when volatility contracts and widening when volatility expands.
Entry conditions
Long if vol_momentum > 0 and price >
SMA.
Short if vol_momentum > 0 and price <
SMA.
Exit conditions
Close if vol_momentum ≤ 0.
Close immediately if ATR-based stop is hit.
This ensures trades are only held during periods when volatility is actively expanding.
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}')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
Captures breakout phases where volatility expands
ATR-based stops adapt to market turbulence
Rolling backtests test resilience across regimes
Limitations
Whipsawed in quiet, range-bound markets
Dependent on parameters like volatility window length
Next steps
Try alternative volatility estimators such as realized volatility or GARCH
Add regime filters to reduce false entries
Extend testing to equities, FX, and commodities
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.