Liquidity Zones: Volume Profile and Order Flow
Detect high-probability trading zones using volume profile, POC, value areas, and order flow imbalances
Introduction
Liquidity zones are price areas with concentrated buying or selling interest. These zones represent where large orders accumulate, creating strong support or resistance.
This lesson covers:
- Volume profile and Point of Control (POC)
- Value Area High/Low detection
- Order flow imbalances
- Liquidity voids and gaps
- Building a liquidity zone detector
Volume Profile Basics
Volume Profile shows volume distribution across price levels (not time):
- Point of Control (POC): Price level with highest volume
- Value Area: Price range containing 70% of volume
- High Volume Node (HVN): Price levels with above-average volume
- Low Volume Node (LVN): Price levels with below-average volume (liquidity voids)
import yfinance as yf
import pandas as pd
import numpy as np
# Download data
df = yf.download('SPY', period='3mo', progress=False)
def calculate_volume_profile(df, num_bins=50):
"""
Calculate volume profile (volume at each price level).
Returns:
DataFrame with price bins and volume
"""
# Create price bins
price_min = df['Low'].min()
price_max = df['High'].max()
bins = np.linspace(price_min, price_max, num_bins)
# Initialize volume profile
volume_profile = pd.DataFrame({
'price_level': (bins[:-1] + bins[1:]) / 2,
'volume': 0.0
})
# Distribute volume across touched price levels
for idx, row in df.iterrows():
# Find bins that price touched during this bar
touched_bins = (bins >= row['Low']) & (bins <= row['High'])
num_touched = touched_bins.sum()
if num_touched > 0:
# Distribute volume evenly across touched bins
volume_per_bin = row['Volume'] / num_touched
for i in range(len(bins) - 1):
if touched_bins[i] or touched_bins[i+1]:
volume_profile.loc[i, 'volume'] += volume_per_bin
return volume_profile
# Calculate volume profile
vp = calculate_volume_profile(df, num_bins=50)
# Find POC (Point of Control)
poc_idx = vp['volume'].idxmax()
poc_price = vp.loc[poc_idx, 'price_level']
poc_volume = vp.loc[poc_idx, 'volume']
print("Volume Profile Analysis:")
print(f"POC (Point of Control): ${poc_price:.2f}")
print(f"POC Volume: {poc_volume:,.0f}")
# Find Value Area (70% of volume)
total_volume = vp['volume'].sum()
target_volume = total_volume * 0.70
# Start from POC and expand until we capture 70% volume
vp_sorted = vp.sort_values('volume', ascending=False)
cumulative_volume = 0
value_area_prices = []
for idx, row in vp_sorted.iterrows():
cumulative_volume += row['volume']
value_area_prices.append(row['price_level'])
if cumulative_volume >= target_volume:
break
vah = max(value_area_prices) # Value Area High
val = min(value_area_prices) # Value Area Low
print(f"\nValue Area High (VAH): ${vah:.2f}")
print(f"Value Area Low (VAL): ${val:.2f}")
print(f"Value Area Width: ${vah - val:.2f} ({(vah-val)/val*100:.1f}%)")
# High Volume Nodes
mean_volume = vp['volume'].mean()
std_volume = vp['volume'].std()
hvn_threshold = mean_volume + std_volume
hvns = vp[vp['volume'] > hvn_threshold].sort_values('volume', ascending=False)
print(f"\nHigh Volume Nodes (HVN): ")
print(hvns[['price_level', 'volume']].head(10))Identifying Liquidity Voids
Liquidity voids (Low Volume Nodes) are price areas with minimal trading activity - price tends to move quickly through these zones:
# Find Low Volume Nodes
lvn_threshold = mean_volume - 0.5 * std_volume
lvns = vp[vp['volume'] < lvn_threshold].sort_values('volume')
print("Liquidity Voids (Low Volume Nodes):")
print(lvns[['price_level', 'volume']].head(10))
# Identify gaps between HVNs
hvn_prices = hvns['price_level'].values
gaps = []
for i in range(len(hvn_prices) - 1):
gap_size = abs(hvn_prices[i+1] - hvn_prices[i])
if gap_size > 2.0: # $2 gap threshold
gaps.append({
'lower': min(hvn_prices[i], hvn_prices[i+1]),
'upper': max(hvn_prices[i], hvn_prices[i+1]),
'size': gap_size
})
print("\nLiquidity Gaps (between HVNs):")
for gap in sorted(gaps, key=lambda x: x['size'], reverse=True)[:5]:
print(f"${gap['lower']:.2f} - ${gap['upper']:.2f} (gap: ${gap['size']:.2f})")Trading Insight: Price tends to move quickly through liquidity voids (LVNs) and slow down at HVNs. Use LVNs for profit targets and HVNs for entry/exit points.
Order Flow Imbalances
Detect supply/demand imbalances using volume and price action:
def detect_order_flow_imbalance(df, window=5):
"""
Detect order flow imbalances using volume and range.
Imbalance = high volume + large range = strong directional pressure
"""
# Calculate metrics
df['Range'] = df['High'] - df['Low']
df['Range_Pct'] = (df['Range'] / df['Close']) * 100
df['Volume_MA'] = df['Volume'].rolling(window=20).mean()
df['Volume_Ratio'] = df['Volume'] / df['Volume_MA']
df['Range_MA'] = df['Range_Pct'].rolling(window=20).mean()
df['Range_Ratio'] = df['Range_Pct'] / df['Range_MA']
# Detect imbalances
df['Bullish_Imbalance'] = (
(df['Close'] > df['Open']) & # Bullish bar
(df['Volume_Ratio'] > 1.5) & # High volume
(df['Range_Ratio'] > 1.2) & # Large range
(df['Close'] > df['High'].shift(1)) # Breakout
)
df['Bearish_Imbalance'] = (
(df['Close'] < df['Open']) & # Bearish bar
(df['Volume_Ratio'] > 1.5) & # High volume
(df['Range_Ratio'] > 1.2) & # Large range
(df['Close'] < df['Low'].shift(1)) # Breakdown
)
return df
# Detect imbalances
df = detect_order_flow_imbalance(df)
print("Order Flow Imbalance Detection:")
print(f"Bullish Imbalances: {df['Bullish_Imbalance'].sum()}")
print(f"Bearish Imbalances: {df['Bearish_Imbalance'].sum()}")
# Show recent imbalances
recent_bull = df[df['Bullish_Imbalance']].tail(3)
print("\nRecent Bullish Imbalances:")
print(recent_bull[['Close', 'Volume_Ratio', 'Range_Ratio']])
recent_bear = df[df['Bearish_Imbalance']].tail(3)
print("\nRecent Bearish Imbalances:")
print(recent_bear[['Close', 'Volume_Ratio', 'Range_Ratio']])Building a Liquidity Zone Detector
Combine volume profile, price levels, and imbalances into a comprehensive detector:
class LiquidityZoneDetector:
"""Detect and classify liquidity zones."""
def __init__(self, df, num_bins=50):
self.df = df
self.vp = calculate_volume_profile(df, num_bins)
self.zones = []
def detect_zones(self):
"""Detect all types of liquidity zones."""
# Calculate statistics
mean_vol = self.vp['volume'].mean()
std_vol = self.vp['volume'].std()
# High Volume Nodes (support/resistance)
hvn_threshold = mean_vol + std_vol
hvns = self.vp[self.vp['volume'] > hvn_threshold]
for idx, row in hvns.iterrows():
self.zones.append({
'price': row['price_level'],
'type': 'HVN',
'strength': row['volume'] / mean_vol,
'role': 'Support/Resistance'
})
# Low Volume Nodes (liquidity voids)
lvn_threshold = mean_vol - 0.5 * std_vol
lvns = self.vp[self.vp['volume'] < lvn_threshold]
for idx, row in lvns.iterrows():
self.zones.append({
'price': row['price_level'],
'type': 'LVN',
'strength': row['volume'] / mean_vol,
'role': 'Liquidity Void'
})
# POC
poc_idx = self.vp['volume'].idxmax()
poc = self.vp.loc[poc_idx]
self.zones.append({
'price': poc['price_level'],
'type': 'POC',
'strength': poc['volume'] / mean_vol,
'role': 'Major Support/Resistance'
})
return pd.DataFrame(self.zones).sort_values('price')
# Use detector
detector = LiquidityZoneDetector(df, num_bins=40)
zones = detector.detect_zones()
print("Liquidity Zone Classification:")
print(f"\nTotal Zones: {len(zones)}")
print(f"HVNs: {(zones['type'] == 'HVN').sum()}")
print(f"LVNs: {(zones['type'] == 'LVN').sum()}")
print(f"POC: {(zones['type'] == 'POC').sum()}")
# Show strongest zones
strong_zones = zones[zones['strength'] > 1.5].sort_values('strength', ascending=False)
print("\nStrongest Liquidity Zones:")
print(strong_zones[['price', 'type', 'strength', 'role']].head(10))
# Current price context
current_price = df['Close'].iloc[-1]
print(f"\nCurrent Price: ${current_price:.2f}")
# Find nearest zones
zones['distance'] = abs(zones['price'] - current_price)
nearest = zones.nsmallest(5, 'distance')
print("\nNearest Liquidity Zones:")
print(nearest[['price', 'type', 'strength', 'distance', 'role']])Summary
Key Takeaways
- Volume Profile distributes volume across price levels, revealing high/low activity zones
- Point of Control (POC) is the price with highest volume - strongest support/resistance
- High Volume Nodes (HVN) are strong support/resistance where price consolidates
- Low Volume Nodes (LVN) are liquidity voids where price moves quickly
- Order flow imbalances (high volume + large range) signal strong directional pressure
- Liquidity zone detectors combine volume profile, price levels, and flow for trading edges
Next Steps
Understanding liquidity zones prepares you for market structure shifts: detecting when trends break and new equilibriums form. The next lesson covers break of structure (BOS) and change of character (ChoCh) patterns.