Volume Profile
Shows where the most trading activity has occurred at each price level — revealing true S/R zones.
What is it?
Volume Profile is a charting tool that displays a horizontal histogram of volume traded at each price level over a specified period, rather than over time. The Point of Control (POC) is the price level with the highest traded volume — it acts as a strong magnet for price. The Value Area (VA) contains 70% of all traded volume and defines the 'fair value' range. The Value Area High (VAH) and Value Area Low (VAL) are key support/resistance levels. Low Volume Nodes (LVNs) are price levels with little traded volume — price tends to move through them quickly. High Volume Nodes (HVNs) are areas of heavy trading — price tends to consolidate around them.
When to use it
- Identifying key support/resistance: the POC and Value Area boundaries (VAH/VAL) are the most significant levels for entries, stops, and targets.
- Predicting price movement speed: price moves quickly through Low Volume Nodes (thin areas) and slowly through High Volume Nodes (thick areas).
- Value area re-entry trades: when price breaks out of the Value Area and then returns to it, the re-entry often leads to a move to the opposite VA boundary.
- Naked POC: a POC from a prior session that price has not revisited is a 'naked POC' — price has a strong tendency to return to it.
- Composite Volume Profile: building a VP over a longer period (e.g. the entire bull cycle) reveals the macro support/resistance structure.
Common pitfalls
- Volume Profile requires reliable, high-quality volume data. On crypto, exchange-specific VP differs significantly from aggregated multi-exchange VP.
- The choice of time period for the VP dramatically changes the result. A daily VP, weekly VP, and cycle VP will show completely different POC and VA levels.
- Volume Profile is a static tool — it shows where volume has been, not where it will be. It must be combined with real-time order flow for high-probability entries.
- On TradingView, the free plan has limited VP functionality. The full fixed range and session VP tools require a paid subscription.
- VP on low-liquidity altcoins is unreliable — thin order books mean a few large trades can create artificial HVNs that do not represent genuine market consensus.
Free code template
Requires Python 3.9+ and the listed pip packages. See inline comments for usage.
# Volume Profile — DAI Python Template
# Requires: pandas, numpy, matplotlib
# Data source: any OHLCV feed (Binance, Coinbase, etc.)
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
def compute_volume_profile(df: pd.DataFrame, n_bins: int = 50) -> pd.DataFrame:
"""
Compute Volume Profile from OHLCV DataFrame.
df must have columns: open, high, low, close, volume
Returns DataFrame with columns: price_level, volume, is_poc, in_value_area
"""
price_min = df['low'].min()
price_max = df['high'].max()
bins = np.linspace(price_min, price_max, n_bins + 1)
bin_centers = (bins[:-1] + bins[1:]) / 2
vol_at_price = np.zeros(n_bins)
for _, row in df.iterrows():
# Distribute candle volume evenly across price range
candle_bins = np.where((bin_centers >= row['low']) & (bin_centers <= row['high']))[0]
if len(candle_bins) > 0:
vol_at_price[candle_bins] += row['volume'] / len(candle_bins)
# Point of Control
poc_idx = np.argmax(vol_at_price)
poc_price = bin_centers[poc_idx]
# Value Area (70% of total volume)
total_vol = vol_at_price.sum()
target_vol = total_vol * 0.70
sorted_idx = np.argsort(vol_at_price)[::-1]
cumvol = 0
va_indices = set()
for idx in sorted_idx:
cumvol += vol_at_price[idx]
va_indices.add(idx)
if cumvol >= target_vol:
break
result = pd.DataFrame({
'price_level': bin_centers,
'volume': vol_at_price,
'is_poc': [i == poc_idx for i in range(n_bins)],
'in_value_area': [i in va_indices for i in range(n_bins)]
})
print(f'POC: {poc_price:.4f} | VAH: {bin_centers[max(va_indices)]:.4f} | VAL: {bin_centers[min(va_indices)]:.4f}')
return result
def plot_volume_profile(df: pd.DataFrame, vp: pd.DataFrame):
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 8), gridspec_kw={'width_ratios': [3, 1]})
ax1.plot(df.index, df['close'], color='#3b82f6', linewidth=1.5)
ax1.set_title('Price', color='white'); ax1.set_facecolor('#0d1f3c')
fig.patch.set_facecolor('#0d1f3c')
colors = ['#fbbf24' if r['is_poc'] else ('#22c55e' if r['in_value_area'] else '#334155')
for _, r in vp.iterrows()]
ax2.barh(vp['price_level'], vp['volume'], height=(vp['price_level'].iloc[1]-vp['price_level'].iloc[0])*0.9,
color=colors, alpha=0.85)
ax2.set_title('Volume Profile', color='white'); ax2.set_facecolor('#0d1f3c')
ax2.tick_params(colors='white'); ax1.tick_params(colors='white')
plt.tight_layout()
plt.savefig('volume_profile.png', dpi=150, bbox_inches='tight')
plt.show()
# Example usage:
# df = pd.read_csv('btc_ohlcv.csv', parse_dates=['timestamp'], index_col='timestamp')
# vp = compute_volume_profile(df, n_bins=60)
# plot_volume_profile(df, vp)