Crypto-Native·Perpetual Futures Mechanics·Intermediate

Perp Liquidation Map

Map the estimated cumulative perpetual futures liquidation levels across the order book by analyzing open interest distribution across leverage tiers, calculating the price levels where cascading forced liquidations could trigger amplified volatility and rapid directional price dislocations.

cryptoperpetual-futures

Perpetual Liquidation Map — Crypto-Native

Category: Crypto-Native | Subcategory: Perpetuals


What This Notebook Does

Perpetual futures liquidations cluster at predictable price levels because exchanges calculate them from standardized leverage tiers. When price reaches a high-density liquidation zone, forced selling amplifies the move — creating liquidation cascades that every trader must understand.

This notebook:

  1. Fetches recent liquidation events from Binance public API (or synthetic fallback)
  2. Bins liquidations into price zones to reveal density hotspots
  3. Identifies cascade-risk zones where clustered liquidations could amplify price moves
  4. Simulates the cascade domino effect if a trigger price is breached
  5. Visualizes a liquidation heatmap with current price overlay
  6. Exports zone data for use in risk management systems

Why Liquidation Maps Matter

ConceptExplanation
Liquidation pricePrice at which the exchange closes a leveraged position by force
Liquidation zonePrice band with unusually high density of liquidation orders
Cascade effectEach liquidation pushes price further, triggering more liquidations
Long liquidationsBelow current price — forced selling accelerates downside
Short liquidationsAbove current price — forced buying accelerates upside
Open interestTotal notional value of outstanding perp positions
[ ]
!pip install numpy pandas matplotlib seaborn requests --quiet
[ ]
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import seaborn as sns
import requests
from typing import Tuple, List
import warnings

warnings.filterwarnings('ignore')
%matplotlib inline
plt.rcParams['figure.figsize'] = (14, 6)
plt.rcParams['axes.spines.top']   = False
plt.rcParams['axes.spines.right'] = False
print('Imports ready.')
Imports ready.
[ ]
# --- Configuration ---
USE_SYNTHETIC       = False       # set True to skip live API calls
SYMBOL              = 'BTCUSDT'
CURRENT_PRICE       = 65_000.0    # override if not fetching live
BIN_SIZE_PCT        = 0.005       # 0.5% price bins for liquidation density
CASCADE_THRESHOLD   = 5_000_000   # USD notional in a bin to flag cascade risk
N_SYNTHETIC         = 2_000       # number of synthetic liquidation events
print('Config ready.')
Config ready.

Section 2 — Data Fetch

generate_synthetic_liquidations Function

This function creates synthetic liquidation events for testing and demonstration purposes when live API data is not available or desired. It generates both long and short liquidation prices and quantities, distributing them around a base price to simulate market behavior.

[ ]
def generate_synthetic_liquidations(
    n: int,
    price_base: float = 65_000.0,
    seed: int = 42
) -> pd.DataFrame:
    """
    Generate synthetic perpetual liquidation events.

    Parameters
    ----------
    n : int
        Number of liquidation events to generate.
    price_base : float
        Centre price around which liquidations are distributed.
    seed : int
        Random seed for reproducibility.

    Returns
    -------
    pd.DataFrame
        Columns: price, side (LONG/SHORT), qty_btc, usd_value, timestamp.

    Notes
    -----
    Long liquidations cluster below price_base (stops hit on downside).
    Short liquidations cluster above price_base (stops hit on upside).
    Distribution is bimodal to reflect typical market structure.
    """
    rng = np.random.default_rng(seed)
    # Long liquidations: below current price, centred -5% with 3% std
    long_prices  = rng.normal(price_base * 0.950, price_base * 0.030, n // 2)
    # Short liquidations: above current price, centred +5% with 3% std
    short_prices = rng.normal(price_base * 1.050, price_base * 0.030, n // 2)

    long_qty  = rng.exponential(0.15, n // 2)  # BTC qty — heavy tail
    short_qty = rng.exponential(0.12, n // 2)

    df_long  = pd.DataFrame({'price': long_prices,  'side': 'LONG',  'qty_btc': long_qty})
    df_short = pd.DataFrame({'price': short_prices, 'side': 'SHORT', 'qty_btc': short_qty})
    df = pd.concat([df_long, df_short], ignore_index=True)
    df['usd_value'] = df['price'] * df['qty_btc']
    df['timestamp'] = pd.date_range('2024-01-01', periods=len(df), freq='1min')
    return df.query('price > 0').reset_index(drop=True)


def fetch_liquidations(
    symbol: str = 'BTCUSDT',
    use_synthetic: bool = False,
    price_base: float = 65_000.0
) -> pd.DataFrame:
    """
    Fetch liquidation events from Binance Futures or fall back to synthetic data.

    Parameters
    ----------
    symbol : str
        Trading pair symbol (e.g., 'BTCUSDT').
    use_synthetic : bool
        If True, skip the API call entirely.
    price_base : float
        Reference price for synthetic generation.

    Returns
    -------
    pd.DataFrame
        Liquidation event dataframe.
    """
    if use_synthetic:
        print('Using synthetic liquidation data.')
        return generate_synthetic_liquidations(N_SYNTHETIC, price_base)

    try:
        # Binance Futures public endpoint — no API key required
        url = f'https://fapi.binance.com/fapi/v1/allForceOrders'
        params = {'symbol': symbol, 'limit': 1000}
        resp = requests.get(url, params=params, timeout=10)
        resp.raise_for_status()
        data = resp.json()
        df = pd.DataFrame(data)
        df = df.rename(columns={'price': 'price', 'origQty': 'qty_btc', 'side': 'side', 'time': 'timestamp'})
        df['price']     = df['price'].astype(float)
        df['qty_btc']   = df['qty_btc'].astype(float)
        df['usd_value'] = df['price'] * df['qty_btc']
        df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
        # 'side' is the side of the liquidated order — SELL means LONG was liquidated
        df['side'] = df['side'].map({'SELL': 'LONG', 'BUY': 'SHORT'})
        print(f'Fetched {len(df)} live liquidation events.')
        return df
    except Exception as e:
        print(f'API failed ({e}), using synthetic data.')
        return generate_synthetic_liquidations(N_SYNTHETIC, price_base)


liq_df = fetch_liquidations(SYMBOL, USE_SYNTHETIC, CURRENT_PRICE)
print(liq_df.head())
print(f'Total USD liquidated: ${liq_df["usd_value"].sum():,.0f}')
API failed (451 Client Error:  for url: https://fapi.binance.com/fapi/v1/allForceOrders?symbol=BTCUSDT&limit=1000), using synthetic data.
          price  side   qty_btc    usd_value           timestamp
0  62344.198306  LONG  0.004656   290.257015 2024-01-01 00:00:00
1  59722.030993  LONG  0.046676  2787.580530 2024-01-01 00:01:00
2  63213.379832  LONG  0.085828  5425.451963 2024-01-01 00:02:00
3  63584.101197  LONG  0.066560  4232.187947 2024-01-01 00:03:00
4  57945.481382  LONG  0.079302  4595.175712 2024-01-01 00:04:00
Total USD liquidated: $17,086,728

fetch_liquidations Function

This function attempts to retrieve live liquidation data from the Binance Futures API. If the API call fails or use_synthetic is set to True in the configuration, it falls back to using the generate_synthetic_liquidations function to provide data. It processes the raw API response into a standardized DataFrame format.

Section 3 — Liquidation Zone Analysis

compute_liquidation_zones Function

This function takes the raw liquidation events and bins them into defined price zones. It calculates the total USD value of long and short liquidations within each price bin, providing a density map of liquidation interest across various price levels.

[ ]
def compute_liquidation_zones(
    liq_df: pd.DataFrame,
    current_price: float,
    bin_size_pct: float = 0.005,
    price_range_pct: float = 0.20
) -> pd.DataFrame:
    """
    Bin liquidation events into price zones and compute density per zone.

    Parameters
    ----------
    liq_df : pd.DataFrame
        Liquidation events with 'price', 'side', 'usd_value' columns.
    current_price : float
        Current market price for relative range calculation.
    bin_size_pct : float
        Width of each price bin as fraction of current price.
    price_range_pct : float
        How far above/below current price to consider (±20% default).

    Returns
    -------
    pd.DataFrame
        Columns: bin_low, bin_mid, bin_high, long_usd, short_usd,
                 total_usd, long_count, short_count.
    """
    lo = current_price * (1 - price_range_pct)
    hi = current_price * (1 + price_range_pct)
    bin_width = current_price * bin_size_pct
    bins = np.arange(lo, hi + bin_width, bin_width)

    liq_df = liq_df[(liq_df['price'] >= lo) & (liq_df['price'] <= hi)].copy()
    liq_df['bin'] = pd.cut(liq_df['price'], bins=bins, labels=False)

    rows = []
    for i in range(len(bins) - 1):
        bucket = liq_df[liq_df['bin'] == i]
        long_b  = bucket[bucket['side'] == 'LONG']
        short_b = bucket[bucket['side'] == 'SHORT']
        rows.append({
            'bin_low':    bins[i],
            'bin_mid':    (bins[i] + bins[i+1]) / 2,
            'bin_high':   bins[i+1],
            'long_usd':   long_b['usd_value'].sum(),
            'short_usd':  short_b['usd_value'].sum(),
            'total_usd':  bucket['usd_value'].sum(),
            'long_count': len(long_b),
            'short_count':len(short_b),
        })
    return pd.DataFrame(rows)


def identify_cascade_zones(
    zone_df: pd.DataFrame,
    current_price: float,
    threshold_usd: float = 5_000_000
) -> pd.DataFrame:
    """
    Identify price zones with cascade risk based on USD notional threshold.

    Parameters
    ----------
    zone_df : pd.DataFrame
        Output of compute_liquidation_zones().
    current_price : float
        Current market price.
    threshold_usd : float
        Minimum USD notional in a zone to flag as cascade risk.

    Returns
    -------
    pd.DataFrame
        Filtered zones above threshold with 'direction' column
        (above/below current price).
    """
    cascade = zone_df[zone_df['total_usd'] >= threshold_usd].copy()
    cascade['direction'] = np.where(cascade['bin_mid'] < current_price, 'below', 'above')
    cascade['cascade_type'] = np.where(
        cascade['direction'] == 'below',
        'Long Liq (bearish pressure)',
        'Short Liq (bullish pressure)'
    )
    return cascade.sort_values('total_usd', ascending=False)


zone_df    = compute_liquidation_zones(liq_df, CURRENT_PRICE, BIN_SIZE_PCT)
cascade_df = identify_cascade_zones(zone_df, CURRENT_PRICE, CASCADE_THRESHOLD)
print(f'Total price zones: {len(zone_df)}')
print(f'Cascade-risk zones: {len(cascade_df)}')
print(cascade_df[['bin_mid', 'total_usd', 'cascade_type']].head(10).to_string())
Total price zones: 80
Cascade-risk zones: 0
Empty DataFrame
Columns: [bin_mid, total_usd, cascade_type]
Index: []

identify_cascade_zones Function

This function identifies price zones that pose a high risk for liquidation cascades. It filters the computed liquidation zones for those where the total notional USD value exceeds a predefined threshold_usd, categorizing them by whether they represent potential long or short liquidation cascades.

Section 4 — Cascade Simulation

simulate_cascade_effect Function

This function simulates the potential price impact of a liquidation cascade. Starting from a trigger_price, it models how each liquidation event in a zone can cause further price movement, potentially triggering more liquidations in adjacent zones, and tracks the cumulative notional value liquidated.

[ ]
def simulate_cascade_effect(
    zone_df: pd.DataFrame,
    trigger_price: float,
    direction: str = 'down',
    cascade_impact_pct: float = 0.002
) -> pd.DataFrame:
    """
    Simulate the price impact of a liquidation cascade.

    Starting from trigger_price, each zone hit adds cascade_impact_pct
    price move per $1M of liquidation notional, potentially triggering
    the next zone.

    Parameters
    ----------
    zone_df : pd.DataFrame
        Output of compute_liquidation_zones().
    trigger_price : float
        Price at which the cascade begins.
    direction : str
        'down' for long liquidations, 'up' for short liquidations.
    cascade_impact_pct : float
        Price impact per $1M notional as fraction of current price.

    Returns
    -------
    pd.DataFrame
        Columns: step, price, zone_mid, notional_triggered, cumulative_notional.
    """
    price = trigger_price
    steps = []
    cum_notional = 0.0

    if direction == 'down':
        # Price falling — long liquidations below trigger get hit
        candidates = zone_df[zone_df['bin_mid'] < trigger_price].sort_values('bin_mid', ascending=False)
        for _, row in candidates.iterrows():
            if price <= row['bin_high']:  # zone is now in range
                notional = row['long_usd']
                impact   = (notional / 1_000_000) * cascade_impact_pct * price
                price   -= impact
                cum_notional += notional
                steps.append({'price': price, 'zone_mid': row['bin_mid'],
                               'notional_triggered': notional, 'cumulative_notional': cum_notional})
    else:
        candidates = zone_df[zone_df['bin_mid'] > trigger_price].sort_values('bin_mid', ascending=True)
        for _, row in candidates.iterrows():
            if price >= row['bin_low']:
                notional = row['short_usd']
                impact   = (notional / 1_000_000) * cascade_impact_pct * price
                price   += impact
                cum_notional += notional
                steps.append({'price': price, 'zone_mid': row['bin_mid'],
                               'notional_triggered': notional, 'cumulative_notional': cum_notional})

    df = pd.DataFrame(steps)
    if len(df):
        df.insert(0, 'step', range(1, len(df) + 1))
    return df


# Simulate downward cascade from -2% below current price
trigger = CURRENT_PRICE * 0.98
cascade_sim = simulate_cascade_effect(zone_df, trigger, direction='down')
if len(cascade_sim):
    final_price = cascade_sim['price'].iloc[-1]
    print(f'Cascade trigger: ${trigger:,.0f}')
    print(f'Cascade final price: ${final_price:,.0f} ({(final_price/CURRENT_PRICE-1)*100:.2f}%)')
    print(f'Total notional liquidated: ${cascade_sim["cumulative_notional"].iloc[-1]:,.0f}')
else:
    print('No cascade triggered from this level.')
Cascade trigger: $63,700
Cascade final price: $63,635 (-2.10%)
Total notional liquidated: $511,006

Section 5 — Visualization

plot_liquidation_heatmap Function

This function visualizes the liquidation data as a heatmap. It displays the density of long and short liquidations across price bins, highlights identified cascade-risk zones, and overlays the current market price, providing a clear graphical representation of potential support/resistance levels due to liquidations.

[ ]
def plot_liquidation_heatmap(
    zone_df: pd.DataFrame,
    current_price: float,
    cascade_zones: pd.DataFrame
) -> None:
    """
    Plot liquidation density heatmap with current price and cascade zones.

    Parameters
    ----------
    zone_df : pd.DataFrame
        All liquidation zones.
    current_price : float
        Current mid price.
    cascade_zones : pd.DataFrame
        High-density cascade zones to highlight.
    """
    fig, axes = plt.subplots(1, 2, figsize=(16, 7))

    # Left: Long vs Short liquidation density bar chart
    ax = axes[0]
    ax.barh(zone_df['bin_mid'], -zone_df['long_usd'] / 1e6,
            height=zone_df['bin_high'] - zone_df['bin_low'],
            color='tomato', alpha=0.8, label='Long Liq (sell pressure)')
    ax.barh(zone_df['bin_mid'], zone_df['short_usd'] / 1e6,
            height=zone_df['bin_high'] - zone_df['bin_low'],
            color='steelblue', alpha=0.8, label='Short Liq (buy pressure)')
    ax.axhline(current_price, color='gold', linewidth=2, linestyle='--', label=f'Current ${current_price:,.0f}')
    for _, row in cascade_zones.iterrows():
        ax.axhspan(row['bin_low'], row['bin_high'], alpha=0.15, color='orange')
    ax.set_xlabel('Notional ($ millions)')
    ax.set_ylabel('Price')
    ax.set_title('Liquidation Density Map')
    ax.legend()

    # Right: Total USD per zone
    ax2 = axes[1]
    colors = ['tomato' if m < current_price else 'steelblue' for m in zone_df['bin_mid']]
    ax2.barh(zone_df['bin_mid'], zone_df['total_usd'] / 1e6,
             height=zone_df['bin_high'] - zone_df['bin_low'],
             color=colors, alpha=0.8)
    ax2.axhline(current_price, color='gold', linewidth=2, linestyle='--')
    # Mark cascade threshold
    ax2.axvline(CASCADE_THRESHOLD / 1e6, color='orange', linewidth=1.5, linestyle=':', label=f'Cascade threshold')
    ax2.set_xlabel('Total Notional ($ millions)')
    ax2.set_title('Total Liquidation Notional per Zone\n(Red=Long liq below, Blue=Short liq above)')
    ax2.legend()

    plt.suptitle(f'BTC Perpetual Liquidation Map | Current Price: ${current_price:,.0f}', fontsize=14)
    plt.tight_layout()
    plt.show()


plot_liquidation_heatmap(zone_df, CURRENT_PRICE, cascade_df)
cell output

Section 6 — Export

export_liquidation_data Function

This function saves the processed liquidation data (raw events, binned zones, and identified cascade risks) into CSV files. This allows for easy external analysis, integration into other systems, or persistent storage of the generated insights.

[ ]
def export_liquidation_data(
    liq_df: pd.DataFrame,
    zone_df: pd.DataFrame,
    cascade_df: pd.DataFrame
) -> None:
    """
    Export liquidation events, zones, and cascade risks to CSV.

    Parameters
    ----------
    liq_df : pd.DataFrame  Raw liquidation events.
    zone_df : pd.DataFrame  Binned price zones.
    cascade_df : pd.DataFrame  High-risk cascade zones.
    """
    liq_df.to_csv('liquidation_events.csv', index=False)
    zone_df.to_csv('liquidation_zones.csv', index=False)
    cascade_df.to_csv('cascade_risk_zones.csv', index=False)
    print('Exported: liquidation_events.csv, liquidation_zones.csv, cascade_risk_zones.csv')


export_liquidation_data(liq_df, zone_df, cascade_df)
Exported: liquidation_events.csv, liquidation_zones.csv, cascade_risk_zones.csv

Summary & Next Steps

Key Takeaways

  • Liquidation maps reveal hidden supply/demand imbalances not visible in the order book
  • Long liquidation clusters below current price act as magnets during downside moves
  • Short liquidation clusters above act as magnets during upside moves
  • Cascade zones — where notional exceeds a threshold — represent high-risk price bands for traders
  • The cascade simulation shows how forced selling can compress into a tight price range rapidly

Limitations

  • Exchange-specific: liquidation data differs across Binance, Bybit, OKX
  • Historical liquidations ≠ future: open interest shifts constantly as new positions open
  • Estimated impact coefficients are simplified; real cascades involve order book depth