Get the full strategy library: This article uses one strategy from the Mega Backtrader Strategy Pack, a package of 500+ Backtrader-ready Python trading strategies with batch backtesting, dashboards, metrics, charts, and documentation.
Download the package here: Mega Backtrader Strategy Pack
If you want to test complete Backtrader strategies without building every idea from scratch, the package gives you strategy code, batch runners, metrics, charts, and publishable reports in one workflow.
Volume profile strategies look at where trading activity happened, not only where price closed. The idea is that high-volume price zones can become important reference areas. Price can reject them, rotate around them, or break away from them.
This article walks through
EnhancedVolumeProfileStrategy, one of the strategies
included in the Mega Backtrader Strategy Pack. The strategy was tested
on UNH daily candles from June 20, 2025 to June 18,
2026, with SPY used as the benchmark.
The result:
| Result | Value |
|---|---|
| Strategy return | 29.51% |
| UNH buy-and-hold return | 32.76% |
| SPY benchmark return | 25.65% |
| Excess return vs SPY | 3.85% |
| Sharpe ratio | 2.06 |
| Max drawdown | 6.79% |
| Closed trades | 8 |
| Open trades | 0 |
| Win rate | 62.50% |
This is a clean example because the run ended with 8 closed trades and no open position. The strategy finished ahead of SPY with a controlled maximum drawdown of 6.79%.
EnhancedVolumeProfileStrategy builds a rolling volume
profile from recent bars, finds the highest-volume price area,
calculates the value area, and then uses those levels as trade
context.
The strategy focuses on four market concepts:
This makes the system different from a simple price-only strategy. It asks where volume concentrated first, then decides whether current price action is interacting with those levels in a tradable way.
The strategy exposes its behavior through Backtrader params:
params = (
('profile_period', 30),
('signal_period', 7),
('value_area_pct', 80),
('price_bins', 30),
('smooth_bins', 3),
('atr_period', 14),
('vpoc_atr_mult', 1.0),
('va_atr_mult', 0.6),
('decay_factor', 0.95),
('volume_confirm_mult', 1.2),
('trend_period', 30),
('pullback_bars', 3),
('trail_stop_pct', 0.02),
)These settings control:
The strategy stores price and volume lines and builds a few core indicators:
self.high = self.data.high
self.low = self.data.low
self.close = self.data.close
self.volume = self.data.volume
self.atr = bt.indicators.ATR(period=self.params.atr_period)
self.trend_ma = bt.indicators.SMA(period=self.params.trend_period)
self.volume_ma = bt.indicators.SMA(self.volume, period=self.params.signal_period)ATR is used to adapt thresholds. The trend moving average gives directional context. The volume moving average is used to confirm whether current volume is meaningful.
The strategy updates adaptive thresholds from ATR:
if not np.isnan(self.atr[0]) and self.atr[0] > 0:
self.vpoc_threshold = (self.atr[0] / self.close[0]) * self.params.vpoc_atr_mult
self.va_threshold = (self.atr[0] / self.close[0]) * self.params.va_atr_mult
else:
self.vpoc_threshold = 0.005
self.va_threshold = 0.003This is important because a fixed price threshold can be too wide in quiet markets and too narrow in volatile markets. ATR makes the level tests scale with current conditions.
The volume profile gives more weight to recent bars through exponential decay:
weight = self.params.decay_factor ** iOlder bars still matter, but recent volume has more influence. That helps the profile adapt as market structure changes.
The profile is built by distributing each bar's weighted volume across its high-low range:
for level in range(num_levels):
price_level = bar_low + (level * (bar_high - bar_low) / num_levels)
price_bin = int((price_level - min_price) / price_step)
binned_price = min_price + price_bin * price_step
raw_profile[binned_price] += volume_per_levelAfter the raw profile is created, the strategy smooths nearby bins:
for i in range(0, len(prices), self.params.smooth_bins):
bin_prices = prices[i:i + self.params.smooth_bins]
total_volume = sum(profile[p] for p in bin_prices)
weighted_price = sum(p * profile[p] for p in bin_prices) / total_volume
smoothed[weighted_price] = total_volumeThat reduces noise. Instead of reacting to tiny adjacent volume differences, the strategy groups neighboring price levels into cleaner zones.
The VPOC is the price level with the most volume:
for price, volume in self.volume_profile.items():
if volume > max_volume:
max_volume = volume
vpoc_price = price
self.vpoc = vpoc_priceThe value area is calculated by sorting levels by volume and accumulating them until the target percentage is reached:
target_volume = self.total_profile_volume * (self.params.value_area_pct / 100)
accumulated_volume = 0
va_levels = []
for price, volume in sorted_levels:
accumulated_volume += volume
va_levels.append(price)
if accumulated_volume >= target_volume:
breakThe highest and lowest selected price levels become the value area high and value area low.
The strategy then classifies current price relative to these profile levels:
if vah_distance <= self.va_threshold:
return 'vah', vah_distance
elif val_distance <= self.va_threshold:
return 'val', val_distanceIt also identifies whether price is inside, above, or below the value area.
Volume confirmation is required before trades:
return self.volume[0] > self.volume_ma[0] * self.params.volume_confirm_multThis helps avoid taking signals when the market is interacting with a level but volume is too quiet to confirm participation.
The breakout logic waits for a pullback instead of chasing the first break:
if current_price > self.value_area_high * (1 + self.va_threshold):
if not self.waiting_for_pullback:
self.breakout_type = 'above_va'
self.breakout_bar = len(self.data)
self.waiting_for_pullback = True
return NoneOnly after enough bars have passed and price pulls back toward the breakout area does the strategy confirm the signal:
if len(self.data) - self.breakout_bar >= self.params.pullback_bars:
self.waiting_for_pullback = False
return 'confirmed_breakout_above'Finally, the strategy uses a 2% trailing stop after entry:
new_trail_stop = current_price * (1 - self.params.trail_stop_pct)
if new_trail_stop > self.trail_stop_price:
self.cancel(self.stop_order)
self.trail_stop_price = new_trail_stop
self.stop_order = self.sell(exectype=bt.Order.Stop, price=self.trail_stop_price)That gives the position room to move while still protecting gains as price advances.
The run used the package batch backtester with these settings:
| Setting | Value |
|---|---|
| Asset | UNH |
| Benchmark | SPY |
| Period | 1y |
| Data window | June 20, 2025 to June 18, 2026 |
| Interval | 1d |
| Starting cash | $10,000 |
| Final value | $12,950.51 |
| Strategy file | EnhancedVolumeProfileStrategy.py |
| Strategy class | EnhancedVolumeProfileStrategy |
| Execution mode | Long-only batch run |
The package generated the metrics table, equity curve, benchmark comparison, drawdown chart, rolling return chart, and daily return distribution automatically.
| Metric | Value |
|---|---|
| Starting portfolio value | $10,000.00 |
| Final portfolio value | $12,950.51 |
| Strategy return | 29.51% |
| UNH buy-and-hold return | 32.76% |
| SPY benchmark return | 25.65% |
| Excess return vs SPY | 3.85% |
| Excess return vs UNH buy-hold | -3.26% |
| Sharpe ratio | 2.06 |
| Max drawdown | 6.79% |
| Total trades | 8 |
| Closed trades | 8 |
| Open trades | 0 |
| Winning trades | 5 |
| Losing trades | 3 |
| Win rate | 62.50% |
| Runtime | 3.39 seconds |
The strategy finished ahead of SPY and kept drawdown below 7%. UNH buy-and-hold finished slightly higher, but the strategy delivered a smoother controlled-risk profile with only 8 completed trades.
The equity curve shows the strategy growing from $10,000 to $12,950.51.
The drawdown chart shows maximum strategy drawdown at 6.79%, which is the strongest part of this case study.
The rolling return chart shows how performance developed across the test window rather than relying only on the final return number.
The daily return distribution excludes zero-return strategy days. In this run, 192 flat strategy-equity days were removed from the histogram, leaving 58 non-zero strategy return days plotted.
This volume-profile example is useful because it demonstrates a different kind of strategy logic.
Many trading systems start with price momentum. This one starts with market structure:
That makes it a good example of why a broad Backtrader strategy library is useful. The package is not limited to one type of signal. It includes trend, momentum, mean reversion, volatility, volume, machine learning, and hybrid approaches.
EnhancedVolumeProfileStrategy is one of 500+
Backtrader-ready strategies included in the Mega
Backtrader Strategy Pack.
Get the full package here: Mega Backtrader Strategy Pack
The package includes:
strategies/
package.If you want to test more strategy ideas faster, this package gives you the code and the research workflow.
Get the Mega Backtrader Strategy Pack
This article is for research and educational use only. Backtest results are not financial advice, investment advice, or a guarantee of future performance. Always validate strategy logic, data quality, execution assumptions, costs, slippage, and risk before using any trading system.