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
'figure.figsize'] = (12, 8)
plt.rcParams[
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
= max(self.params.vol_window, self.params.price_wma_window, self.params.atr_window)
min_periods if len(self) < min_periods:
return
= self.vol_momentum[0]
vol_momentum = self.data.close[0]
current_price = self.price_wma[0]
wma_price
# 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)
= self.position.size
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
= current_price - (self.atr[0] * self.params.atr_multiplier)
new_stop 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
= current_price + (self.atr[0] * self.params.atr_multiplier)
new_stop 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 or self.datas[0].datetime.date(0)
dt 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(
=self.profit_target_price,
price=bt.Order.Limit
exectype
)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(
=self.profit_target_price,
price=bt.Order.Limit
exectype
)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:
= (trade.pnl / abs(trade.value)) * 100 if trade.value != 0 else 0
profit_pct 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}...")
= yf.download(symbol, start=start_date, end=end_date)
data
if data.empty:
print("No data downloaded. Please check the symbol and date range.")
return None, None
# Create backtrader data feed
= bt.feeds.PandasData(dataname=data)
data_feed
# Initialize cerebro
= bt.Cerebro()
cerebro
cerebro.adddata(data_feed)
cerebro.addstrategy(VolMomWMAProfitTargetStrategy)
# Set initial capital and commission
cerebro.broker.setcash(initial_cash)=0.001) # 0.1% commission
cerebro.broker.setcommission(commission
# Add analyzers
='sharpe')
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='drawdown')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='returns')
cerebro.addanalyzer(bt.analyzers.Returns, _name='trades')
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name
print(f'Starting Portfolio Value: ${cerebro.broker.getvalue():.2f}')
# Run strategy
= cerebro.run()
results
= cerebro.broker.getvalue()
final_value print(f'Final Portfolio Value: ${final_value:.2f}')
print(f'Total Return: {((final_value - initial_cash) / initial_cash) * 100:.2f}%')
# Print analyzer results
= results[0]
strat
# Sharpe Ratio
= strat.analyzers.sharpe.get_analysis()
sharpe_analysis = sharpe_analysis.get('sharperatio', 'N/A')
sharpe print(f'Sharpe Ratio: {sharpe:.3f}' if sharpe != 'N/A' else f'Sharpe Ratio: {sharpe}')
# Drawdown
= strat.analyzers.drawdown.get_analysis()
drawdown_analysis = drawdown_analysis.get('max', {}).get('drawdown', 'N/A')
max_drawdown print(f'Max Drawdown: {max_drawdown:.2f}%' if max_drawdown != 'N/A' else f'Max Drawdown: {max_drawdown}')
# Trade Analysis
= strat.analyzers.trades.get_analysis()
trade_analysis = trade_analysis.get('total', {}).get('total', 0)
total_trades = trade_analysis.get('won', {}).get('total', 0)
won_trades = trade_analysis.get('lost', {}).get('total', 0)
lost_trades = (won_trades / total_trades * 100) if total_trades > 0 else 0
win_rate
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:
= trade_analysis.get('won', {}).get('pnl', {}).get('average', 0)
avg_win print(f'Average Win: ${avg_win:.2f}')
if lost_trades > 0:
= trade_analysis.get('lost', {}).get('pnl', {}).get('average', 0)
avg_loss print(f'Average Loss: ${avg_loss:.2f}')
# Plot results
try:
='candlestick', volume=False)
cerebro.plot(style
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.