Liquidity Zones: Volume Profile and Order Flow

Detect high-probability trading zones using volume profile, POC, value areas, and order flow imbalances

30 min read
Advanced

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)
python
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:

python
# 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:

python
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:

python
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

  1. Volume Profile distributes volume across price levels, revealing high/low activity zones
  2. Point of Control (POC) is the price with highest volume - strongest support/resistance
  3. High Volume Nodes (HVN) are strong support/resistance where price consolidates
  4. Low Volume Nodes (LVN) are liquidity voids where price moves quickly
  5. Order flow imbalances (high volume + large range) signal strong directional pressure
  6. 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.