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.
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:
- Fetches recent liquidation events from Binance public API (or synthetic fallback)
- Bins liquidations into price zones to reveal density hotspots
- Identifies cascade-risk zones where clustered liquidations could amplify price moves
- Simulates the cascade domino effect if a trigger price is breached
- Visualizes a liquidation heatmap with current price overlay
- Exports zone data for use in risk management systems
Why Liquidation Maps Matter
| Concept | Explanation |
|---|---|
| Liquidation price | Price at which the exchange closes a leveraged position by force |
| Liquidation zone | Price band with unusually high density of liquidation orders |
| Cascade effect | Each liquidation pushes price further, triggering more liquidations |
| Long liquidations | Below current price — forced selling accelerates downside |
| Short liquidations | Above current price — forced buying accelerates upside |
| Open interest | Total notional value of outstanding perp positions |
!pip install numpy pandas matplotlib seaborn requests --quietimport 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)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