The VolMomWMAProfitTargetStrategy is an advanced
quantitative trading system that seeks to capitalize on market
momentum by combining volatility trends with price direction, utilizing
a Weighted Moving Average (WMA) for trend identification and
incorporating a fixed profit target alongside a trailing stop-loss for
risk management.
This strategy aims to identify periods where there’s a significant increase in price volatility that is also aligned with the underlying price trend. It seeks to enter trades during these “strong volatility momentum” periods and manage them with predefined exit conditions.
An entry is initiated when two primary conditions are met:
vol_momentum_window periods ago. A positive and
sufficiently large vol_momentum (exceeding
vol_momentum_threshold) indicates that market volatility is
accelerating, suggesting a potentially strong directional move is
underway or impending.price_wma.price_wma.Both of these conditions must be satisfied simultaneously for a trade order to be placed.
The strategy employs a dual-pronged approach to position management:
profit_target_percent. For short positions, it’s the entry
price minus the profit_target_percent. If the price reaches
this target, the position is closed, securing profits.vol_momentum turns negative or falls below a certain
threshold. This acts as an adaptive exit, indicating that the driving
force behind the trade might be dissipating.PctChange): Calculates the
percentage change in price, used as input for volatility.StandardDeviation):
Measures market volatility based on the standard deviation of returns
over a vol_window period.vol_momentum_window periods ago. A positive value indicates
increasing volatility.WeightedMovingAverage):
Identifies the primary price trend, giving more weight to recent
prices.AverageTrueRange): Measures
market volatility, used for setting and adjusting the trailing
stop.The strategy is implemented in backtrader as
follows:
import backtrader as bt
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = (12, 8)
class VolMomWMAProfitTargetStrategy(bt.Strategy):
"""
Advanced Volatility Momentum: Uses Weighted Moving Average for trend,
and incorporates a fixed profit target.
"""
params = (
('vol_window', 30),
('vol_momentum_window', 7),
('price_wma_window', 90),
('atr_window', 7),
('atr_multiplier', 5.), # risk management
('profit_target_percent', 0.05), # profit target (was 5%, often too ambitious)
('vol_momentum_threshold', 0.001), # Minimum threshold to filter noise
)
def __init__(self):
self.returns = bt.indicators.PctChange(self.data.close, period=1)
self.volatility = bt.indicators.StandardDeviation(self.returns, period=self.params.vol_window)
self.vol_momentum = self.volatility - self.volatility(-self.params.vol_momentum_window)
# Price trend = Weighted Moving Average
self.price_wma = bt.indicators.WeightedMovingAverage(self.data.close, period=self.params.price_wma_window)
self.atr = bt.indicators.ATR(self.data, period=self.params.atr_window)
# Trade management variables
self.stop_price = 0
self.profit_target_price = 0
self.trade_count = 0
self.entry_price = 0
# Order tracking
self.order_ref = None # Entry order
self.profit_order_ref = None # Profit target order
self.stop_order_ref = None # Stop loss order
def next(self):
# Ensure enough data for all indicators
min_periods = max(self.params.vol_window, self.params.price_wma_window, self.params.atr_window)
if len(self) < min_periods:
return
vol_momentum = self.vol_momentum[0]
current_price = self.data.close[0]
wma_price = self.price_wma[0]
# Skip if we have pending entry orders
if self.order_ref:
return
# Position management
if self.position:
# Manual stop loss check (backup in case stop order fails)
position_size = self.position.size
if position_size > 0 and current_price <= self.stop_price:
self._close_position(f'MANUAL STOP LOSS - Long closed at {current_price:.2f}')
return
elif position_size < 0 and current_price >= self.stop_price:
self._close_position(f'MANUAL STOP LOSS - Short closed at {current_price:.2f}')
return
# Exit if volatility momentum turns negative or becomes too weak
if vol_momentum <= -self.params.vol_momentum_threshold:
self._close_position(f'VOL MOMENTUM REVERSAL - Vol momentum: {vol_momentum:.6f}')
return
# Update trailing stop (only move in favorable direction)
if position_size > 0: # Long position
new_stop = current_price - (self.atr[0] * self.params.atr_multiplier)
if new_stop > self.stop_price: # Only move up
self.stop_price = new_stop
self.log(f'TRAILING STOP UPDATED - New stop: {self.stop_price:.2f}')
elif position_size < 0: # Short position
new_stop = current_price + (self.atr[0] * self.params.atr_multiplier)
if new_stop < self.stop_price: # Only move down
self.stop_price = new_stop
self.log(f'TRAILING STOP UPDATED - New stop: {self.stop_price:.2f}')
# Entry signals: Strong vol momentum + price direction confirmation
if not self.position and vol_momentum > self.params.vol_momentum_threshold:
# Long entry: price above WMA and strong vol momentum
if current_price > wma_price:
self.order_ref = self.buy()
self.stop_price = current_price - (self.atr[0] * self.params.atr_multiplier)
self.profit_target_price = current_price * (1 + self.params.profit_target_percent)
self.trade_count += 1
self.log(f'LONG SIGNAL - Price: {current_price:.2f}, WMA: {wma_price:.2f}, Vol Mom: {vol_momentum:.6f}, Stop: {self.stop_price:.2f}, Target: {self.profit_target_price:.2f}')
# Short entry: price below WMA and strong vol momentum
elif current_price < wma_price:
self.order_ref = self.sell()
self.stop_price = current_price + (self.atr[0] * self.params.atr_multiplier)
self.profit_target_price = current_price * (1 - self.params.profit_target_percent)
self.trade_count += 1
self.log(f'SHORT SIGNAL - Price: {current_price:.2f}, WMA: {wma_price:.2f}, Vol Mom: {vol_momentum:.6f}, Stop: {self.stop_price:.2f}, Target: {self.profit_target_price:.2f}')
def _close_position(self, message):
"""Helper method to close position and cancel pending orders"""
if self.position:
self.close()
self.log(message)
# Cancel any pending profit target orders
if self.profit_order_ref:
self.cancel(self.profit_order_ref)
self.profit_order_ref = None
# Cancel any pending stop orders
if self.stop_order_ref:
self.cancel(self.stop_order_ref)
self.stop_order_ref = None
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.Submitted, order.Accepted]:
return
if order.status == order.Completed:
# Entry order completed
if self.order_ref and order.ref == self.order_ref.ref:
self.entry_price = order.executed.price
if order.isbuy():
# Long position - set profit target as sell limit order
self.profit_order_ref = self.sell(
price=self.profit_target_price,
exectype=bt.Order.Limit
)
self.log(f'LONG EXECUTED - Entry: {self.entry_price:.2f}, Target: {self.profit_target_price:.2f}')
elif order.issell():
# Short position - set profit target as buy limit order
self.profit_order_ref = self.buy(
price=self.profit_target_price,
exectype=bt.Order.Limit
)
self.log(f'SHORT EXECUTED - Entry: {self.entry_price:.2f}, Target: {self.profit_target_price:.2f}')
self.order_ref = None # Clear entry order reference
# Profit target order completed
elif self.profit_order_ref and order.ref == self.profit_order_ref.ref:
self.log(f'PROFIT TARGET HIT - Exit: {order.executed.price:.2f}')
self.profit_order_ref = None
# Position is now closed, reset tracking variables
self.stop_price = 0
self.profit_target_price = 0
self.entry_price = 0
elif order.status in [order.Canceled, order.Rejected]:
# Handle canceled/rejected orders
if self.order_ref and order.ref == self.order_ref.ref:
self.log(f'ENTRY ORDER CANCELED/REJECTED - Status: {order.getstatusname()}')
self.order_ref = None
elif self.profit_order_ref and order.ref == self.profit_order_ref.ref:
self.log(f'PROFIT TARGET ORDER CANCELED/REJECTED - Status: {order.getstatusname()}')
self.profit_order_ref = None
elif self.stop_order_ref and order.ref == self.stop_order_ref.ref:
self.log(f'STOP ORDER CANCELED/REJECTED - Status: {order.getstatusname()}')
self.stop_order_ref = None
def notify_trade(self, trade):
if trade.isclosed:
profit_pct = (trade.pnl / abs(trade.value)) * 100 if trade.value != 0 else 0
self.log(f'TRADE CLOSED - PnL: ${trade.pnl:.2f} ({profit_pct:.2f}%)')
def stop(self):
print(f'\n=== WMA VOLATILITY MOMENTUM WITH PROFIT TARGET RESULTS ===')
print(f'Total Trades: {self.trade_count}')
print(f'Strategy: Vol momentum with WMA trend and Profit Target')
print(f'Parameters: Vol={self.params.vol_window}d, Vol_Mom={self.params.vol_momentum_window}d, WMA={self.params.price_wma_window}d')
print(f'Risk Management: ATR {self.params.atr_window}d × {self.params.atr_multiplier}, Profit Target: {self.params.profit_target_percent:.2%}')
print(f'Vol Momentum Threshold: {self.params.vol_momentum_threshold}')The strategy’s behavior is customized through its parameters:
vol_window: The lookback period for
calculating price volatility (Standard Deviation of returns).vol_momentum_window: The lookback
period for calculating volatility momentum (current volatility minus
volatility N periods ago).price_wma_window: The period for the
Weighted Moving Average (WMA), used to identify the primary price
trend.atr_window: The period for the Average
True Range (ATR), used in risk management for the trailing stop.atr_multiplier: A multiplier applied
to the ATR to determine the distance of the trailing stop.profit_target_percent: A percentage
defining the fixed profit target. When reached, the position is
closed.vol_momentum_threshold: A minimum
threshold for volatility momentum. Signals are only considered if
volatility momentum exceeds this value, filtering out noise.__init__)In the __init__ method, all necessary indicators and
internal state variables are set up:
self.returns: Calculates daily
percentage changes in the closing price.self.volatility: Measures the standard
deviation of returns over vol_window, representing market
volatility.self.vol_momentum: Calculated as the
difference between current volatility and past volatility, indicating
whether volatility is increasing or decreasing.self.price_wma: The Weighted Moving
Average of the closing price, which gives more weight to recent data
points, making it responsive to current trends.self.atr: The Average True Range
indicator, used for dynamic stop-loss calculations.stop_price, profit_target_price,
trade_count, and entry_price are initialized
to manage open positions and track trade statistics.order_ref,
profit_order_ref, and stop_order_ref are used
to hold references to pending orders, allowing for proper management and
cancellation.next)The next method contains the core trading logic and is
executed on each new bar of data:
order_ref (meaning an entry order is still pending
submission or acceptance), the method returns to avoid
placing multiple orders.if self.position): If a position is currently
open:
self.stop_price. This acts as a backup in
case the Stop or StopTrail order placed via
backtrader’s broker mechanism doesn’t trigger immediately
due to specific order execution models or market gaps.vol_momentum falls below or equals a negative
vol_momentum_threshold, the position is closed using the
_close_position helper method. This acts as an adaptive
exit, indicating that the driving force behind the trade might be
dissipating.stop_price
is updated. For long positions, it only moves up (higher), always
trailing current_price by
atr_multiplier * atr[0]. For short positions, it only moves
down (lower). This dynamically locks in profits.if not self.position):
If no position is currently open and vol_momentum is above
vol_momentum_threshold:
current_price is
above wma_price, a buy order is placed.
The stop_price and profit_target_price are
immediately set.current_price is
below wma_price, a sell order is placed.
The stop_price and profit_target_price are
immediately set.trade_count is incremented, and detailed logs are
printed for the signal and initial exit levels._close_position)log)notify_order)This method is called by backtrader whenever an order’s
status changes. It plays a critical role in managing the strategy’s
active orders:
buy or sell order
(self.order_ref) is Completed, the
entry_price is recorded, and a limit order
for the profit target
(self.profit_order_ref) is placed.self.profit_order_ref is Completed, it
confirms the profit target was hit. All related tracking variables
(stop_price, profit_target_price,
entry_price) are reset.entry, profit target, or
stop) fails to complete, logging the event and clearing the
respective order references.notify_trade)stop)The provided run_vol_mom_wma_strategy function
demonstrates how to set up and execute a backtest for the
VolMomWMAProfitTargetStrategy using historical data from
yfinance.
# Example usage and backtesting function
def run_vol_mom_wma_strategy(symbol='SPY', start_date='2020-01-01', end_date='2024-01-01', initial_cash=10000):
"""
Function to run the Vol Momentum WMA strategy with example parameters
"""
# Download data
print(f"Downloading data for {symbol} from {start_date} to {end_date}...")
data = yf.download(symbol, start=start_date, end=end_date)
if data.empty:
print("No data downloaded. Please check the symbol and date range.")
return None, None
# Create backtrader data feed
data_feed = bt.feeds.PandasData(dataname=data)
# Initialize cerebro
cerebro = bt.Cerebro()
cerebro.adddata(data_feed)
cerebro.addstrategy(VolMomWMAProfitTargetStrategy)
# Set initial capital and commission
cerebro.broker.setcash(initial_cash)
cerebro.broker.setcommission(commission=0.001) # 0.1% commission
# Add analyzers
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')
print(f'Starting Portfolio Value: ${cerebro.broker.getvalue():.2f}')
# Run strategy
results = cerebro.run()
final_value = cerebro.broker.getvalue()
print(f'Final Portfolio Value: ${final_value:.2f}')
print(f'Total Return: {((final_value - initial_cash) / initial_cash) * 100:.2f}%')
# Print analyzer results
strat = results[0]
# Sharpe Ratio
sharpe_analysis = strat.analyzers.sharpe.get_analysis()
sharpe = sharpe_analysis.get('sharperatio', 'N/A')
print(f'Sharpe Ratio: {sharpe:.3f}' if sharpe != 'N/A' else f'Sharpe Ratio: {sharpe}')
# Drawdown
drawdown_analysis = strat.analyzers.drawdown.get_analysis()
max_drawdown = drawdown_analysis.get('max', {}).get('drawdown', 'N/A')
print(f'Max Drawdown: {max_drawdown:.2f}%' if max_drawdown != 'N/A' else f'Max Drawdown: {max_drawdown}')
# Trade Analysis
trade_analysis = strat.analyzers.trades.get_analysis()
total_trades = trade_analysis.get('total', {}).get('total', 0)
won_trades = trade_analysis.get('won', {}).get('total', 0)
lost_trades = trade_analysis.get('lost', {}).get('total', 0)
win_rate = (won_trades / total_trades * 100) if total_trades > 0 else 0
print(f'Total Trades: {total_trades}')
print(f'Winning Trades: {won_trades}')
print(f'Losing Trades: {lost_trades}')
print(f'Win Rate: {win_rate:.1f}%')
if won_trades > 0:
avg_win = trade_analysis.get('won', {}).get('pnl', {}).get('average', 0)
print(f'Average Win: ${avg_win:.2f}')
if lost_trades > 0:
avg_loss = trade_analysis.get('lost', {}).get('pnl', {}).get('average', 0)
print(f'Average Loss: ${avg_loss:.2f}')
# Plot results
try:
cerebro.plot(style='candlestick', volume=False)
plt.show()
except Exception as e:
print(f"Plotting failed: {e}")
return cerebro, results
# Uncomment to run the strategy:
# if __name__ == '__main__':
# cerebro, results = run_vol_mom_wma_strategy(symbol='SPY', start_date='2022-01-01', end_date='2024-01-01')yfinance.download to fetch historical price data for a
given symbol and date_range. It includes a
check for empty data.backtrader.Cerebro
instance is initialized. The downloaded data is added as a feed, and the
VolMomWMAProfitTargetStrategy is added to Cerebro.backtrader.analyzers are added to provide detailed
performance metrics such as Sharpe Ratio, Drawdown, Total Returns, and
Trade Analysis (e.g., total trades, win/loss count, average
win/loss).cerebro.run()
executes the backtest. The final portfolio value and total return are
printed.cerebro.plot() is called to
visualize the backtest results, showing price action and trade
entries/exits. A try-except block is included for
robustness in case plotting fails.
The VolMomWMAProfitTargetStrategy provides a dynamic and
comprehensive approach to momentum-based trading. By integrating
volatility momentum as a key driver for entry,
confirming trends with a Weighted Moving Average, and
employing a robust exit framework that includes both a
fixed profit target and an adaptive trailing stop, the strategy aims to
capture significant moves while effectively managing risk. The detailed
logging and analytical outputs from the backtesting function allow for
thorough evaluation of its performance characteristics across various
market conditions.