Liquidity Filter
Filter backtest trade executions by available order book liquidity depth to ensure strategies only hypothetically execute when there is sufficient resting liquidity at the expected price, avoiding unrealistic fills in illiquid market conditions.
Liquidity Filter
Introduction
In financial markets, liquidity refers to the ease with which an asset can be converted into cash without affecting its market price. A highly liquid asset can be bought or sold quickly without causing significant price movements, while an illiquid asset may be difficult to trade without impacting its price.
A Liquidity Filter is a mechanism used in quantitative finance and algorithmic trading to select or exclude financial instruments (e.g., stocks, currencies, commodities) or time periods based on their trading activity or market depth. The primary purpose of a liquidity filter is to ensure that any trading strategy or analysis is applied to assets or market conditions that are sufficiently liquid, thereby reducing undesirable effects such as high transaction costs, significant slippage, and difficulty in executing large orders.
Why is a Liquidity Filter Important?
- Minimizing Slippage: Slippage occurs when the actual execution price of a trade deviates from the expected price. In illiquid markets, even small orders can move the price significantly, leading to higher slippage.
- Reducing Transaction Costs: Bid-ask spreads are typically wider for illiquid assets, increasing the cost of trading.
- Improving Execution Quality: Trading in liquid markets generally allows for faster and more reliable order execution.
- Focusing Analysis: By filtering out illiquid assets or data points, analysts can focus their attention on markets where their strategies are more likely to be executable and profitable.
This notebook will demonstrate how to implement and visualize different types of liquidity filters using Python.
Data Generation and Setup
First, let's set up our environment and generate some mock financial data that includes price and volume, which are key components for applying liquidity filters. We'll simulate data for a hypothetical asset over a period.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# Set random seed for reproducibility
np.random.seed(42)
# Generate mock time-series data
dates = pd.date_range(start='2023-01-01', periods=200, freq='H')
# Simulate price data with some trend and noise
price = 100 + np.cumsum(np.random.randn(len(dates)) * 0.1) + np.sin(np.arange(len(dates)) * 0.05) * 5
# Simulate volume data with periods of high and low liquidity
# Create periods of low volume (illiquidity)
volume = np.random.randint(500, 5000, len(dates)).astype(float)
volume[50:70] = np.random.randint(50, 200, 20) # Low liquidity period 1
volume[120:140] = np.random.randint(50, 250, 20) # Low liquidity period 2
# Create a DataFrame
df = pd.DataFrame({'date': dates, 'price': price, 'volume': volume})
df = df.set_index('date')
print("Generated mock financial data:")
display(df.head())
print(f"\nTotal data points: {len(df)}")Generated mock financial data:
/tmp/ipykernel_3108/2692194961.py:9: FutureWarning: 'H' is deprecated and will be removed in a future version, please use 'h' instead. dates = pd.date_range(start='2023-01-01', periods=200, freq='H')
| price | volume | |
|---|---|---|
| date | ||
| 2023-01-01 00:00:00 | 100.049671 | 1909.0 |
| 2023-01-01 01:00:00 | 100.285741 | 4557.0 |
| 2023-01-01 02:00:00 | 100.599781 | 1284.0 |
| 2023-01-01 03:00:00 | 101.000107 | 3675.0 |
| 2023-01-01 04:00:00 | 101.222848 | 4841.0 |
Total data points: 200
Static Volume-Based Liquidity Filter
A common and straightforward liquidity filter is based on a static volume threshold. This method simply keeps data points (or assets) where the trading volume exceeds a predefined minimum level. Data points with volume below this threshold are considered illiquid and are discarded from the analysis or trading universe.
Function Definition
Below is a Python function apply_static_volume_liquidity_filter that takes a DataFrame, the name of the volume column, and a volume threshold as input. It returns a new DataFrame containing only the data points that meet the liquidity criterion.
def apply_static_volume_liquidity_filter(df: pd.DataFrame, volume_column: str, threshold: float) -> pd.DataFrame:
"""
Applies a static volume-based liquidity filter to a DataFrame.
Args:
df (pd.DataFrame): The input DataFrame containing financial data.
volume_column (str): The name of the column representing trading volume.
threshold (float): The minimum volume required for a data point to be considered liquid.
Returns:
pd.DataFrame: A new DataFrame containing only the liquid data points.
"""
if volume_column not in df.columns:
raise ValueError(f"Volume column '{volume_column}' not found in DataFrame.")
# Filter the DataFrame based on the volume threshold
filtered_df = df[df[volume_column] >= threshold].copy()
return filtered_df
# Example Usage:
static_volume_threshold = 1000 # Example threshold
filtered_df_static = apply_static_volume_liquidity_filter(df, 'volume', static_volume_threshold)
print(f"Original data points: {len(df)}")
print(f"Data points after static filter (volume >= {static_volume_threshold}): {len(filtered_df_static)}")
display(filtered_df_static.head())Original data points: 200 Data points after static filter (volume >= 1000): 153
| price | volume | |
|---|---|---|
| date | ||
| 2023-01-01 00:00:00 | 100.049671 | 1909.0 |
| 2023-01-01 01:00:00 | 100.285741 | 4557.0 |
| 2023-01-01 02:00:00 | 100.599781 | 1284.0 |
| 2023-01-01 03:00:00 | 101.000107 | 3675.0 |
| 2023-01-01 04:00:00 | 101.222848 | 4841.0 |
Visualization 1: Effect of Static Liquidity Filter
This visualization shows the original price and volume data alongside the data points that remain after applying the static liquidity filter. You can observe how periods of low volume are effectively removed, leaving only the more liquid observations.
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), sharex=True)
# Plot original price data
ax1.plot(df.index, df['price'], label='Original Price', color='skyblue', alpha=0.7)
ax1.scatter(filtered_df_static.index, filtered_df_static['price'],
color='red', marker='o', s=10, label=f'Filtered Price (Volume >= {static_volume_threshold})')
ax1.set_title('Price Data: Original vs. Statically Filtered')
ax1.set_ylabel('Price')
ax1.legend()
ax1.grid(True, linestyle='--', alpha=0.6)
# Plot original volume and the threshold
ax2.plot(df.index, df['volume'], label='Original Volume', color='lightgray', alpha=0.8)
ax2.axhline(y=static_volume_threshold, color='green', linestyle='--', label='Static Volume Threshold')
ax2.fill_between(df.index, 0, df['volume'], where=df['volume'] < static_volume_threshold,
color='orange', alpha=0.3, label='Illiquid Periods')
ax2.set_title('Volume Data with Static Liquidity Threshold')
ax2.set_xlabel('Date')
ax2.set_ylabel('Volume')
ax2.legend()
ax2.grid(True, linestyle='--', alpha=0.6)
plt.tight_layout()
plt.show()
print("\nInterpretation:")
print("The top plot shows that the red circles (filtered price points) are only present when the volume is above the static threshold. The bottom plot clearly indicates the illiquid periods (shaded orange) where the volume falls below the green dashed line threshold. This filter effectively removes data from these less active periods, ensuring that subsequent analysis focuses solely on sufficiently liquid market conditions.")Interpretation: The top plot shows that the red circles (filtered price points) are only present when the volume is above the static threshold. The bottom plot clearly indicates the illiquid periods (shaded orange) where the volume falls below the green dashed line threshold. This filter effectively removes data from these less active periods, ensuring that subsequent analysis focuses solely on sufficiently liquid market conditions.
Rolling Average Volume-Based Liquidity Filter
A static volume threshold might not always be optimal, as market liquidity can change dynamically over time. A rolling average volume-based liquidity filter addresses this by making the threshold adaptive. Instead of a fixed number, the filter compares the current volume (or the previous period's volume) against a rolling average of past volumes.
This approach allows the filter to adjust to evolving market conditions, becoming more lenient during generally lower volume periods and stricter during higher volume periods, relative to its own recent history. A common implementation involves filtering based on a multiple of the rolling average volume (e.g., current volume must be at least 0.5 times the 20-period rolling average volume).
def apply_rolling_volume_liquidity_filter(df: pd.DataFrame, volume_column: str, window: int, threshold_multiplier: float) -> pd.DataFrame:
"""
Applies a rolling average volume-based liquidity filter to a DataFrame.
Args:
df (pd.DataFrame): The input DataFrame containing financial data.
volume_column (str): The name of the column representing trading volume.
window (int): The number of periods for the rolling average calculation.
threshold_multiplier (float): A multiplier applied to the rolling average volume
to determine the adaptive threshold.
Returns:
pd.DataFrame: A new DataFrame containing only the liquid data points.
"""
if volume_column not in df.columns:
raise ValueError(f"Volume column '{volume_column}' not found in DataFrame.")
# Calculate the rolling average volume
df['rolling_avg_volume'] = df[volume_column].rolling(window=window, min_periods=1).mean()
# Calculate the adaptive threshold
df['adaptive_threshold'] = df['rolling_avg_volume'] * threshold_multiplier
# Apply the filter: current volume must be greater than or equal to the adaptive threshold
filtered_df = df[df[volume_column] >= df['adaptive_threshold']].copy()
# Drop auxiliary columns before returning
filtered_df = filtered_df.drop(columns=['rolling_avg_volume', 'adaptive_threshold'])
return filtered_df
# Example Usage:
rolling_window = 20 # 20-period rolling average
rolling_threshold_multiplier = 0.8 # Current volume must be at least 80% of the 20-period rolling average
filtered_df_rolling = apply_rolling_volume_liquidity_filter(
df.copy(), 'volume', rolling_window, rolling_threshold_multiplier
)
print(f"Original data points: {len(df)}")
print(f"Data points after rolling filter (volume >= {rolling_threshold_multiplier} * {rolling_window}-period rolling avg): {len(filtered_df_rolling)}")
display(filtered_df_rolling.head())Original data points: 200 Data points after rolling filter (volume >= 0.8 * 20-period rolling avg): 105
| price | volume | |
|---|---|---|
| date | ||
| 2023-01-01 00:00:00 | 100.049671 | 1909.0 |
| 2023-01-01 01:00:00 | 100.285741 | 4557.0 |
| 2023-01-01 03:00:00 | 101.000107 | 3675.0 |
| 2023-01-01 04:00:00 | 101.222848 | 4841.0 |
| 2023-01-01 05:00:00 | 101.443108 | 3306.0 |
Visualization 2: Comparing Static vs. Rolling Liquidity Filters
This visualization compares the behavior of the static threshold filter against the rolling average threshold filter. It highlights how the rolling threshold adapts to the historical volume, potentially offering a more nuanced approach to identifying liquid periods than a fixed static value.
# Recalculate rolling average and adaptive threshold for plotting
df_plot = df.copy()
df_plot['rolling_avg_volume'] = df_plot['volume'].rolling(window=rolling_window, min_periods=1).mean()
df_plot['adaptive_threshold'] = df_plot['rolling_avg_volume'] * rolling_threshold_multiplier
fig, ax = plt.subplots(figsize=(14, 7))
# Plot original volume
ax.plot(df_plot.index, df_plot['volume'], label='Original Volume', color='lightgray', alpha=0.8)
# Plot static threshold
ax.axhline(y=static_volume_threshold, color='green', linestyle='--', label=f'Static Threshold ({static_volume_threshold})')
# Plot rolling adaptive threshold
ax.plot(df_plot.index, df_plot['adaptive_threshold'], color='purple', linestyle=':',
label=f'Rolling Adaptive Threshold ({rolling_threshold_multiplier} * {rolling_window}-period avg)')
# Indicate periods filtered by static
ax.fill_between(df_plot.index, 0, df_plot['volume'], where=df_plot['volume'] < static_volume_threshold,
color='red', alpha=0.1, label='Filtered by Static Threshold')
# Indicate periods filtered by rolling (only where static wouldn't catch it, for clarity)
ax.fill_between(df_plot.index, 0, df_plot['volume'],
where=(df_plot['volume'] < df_plot['adaptive_threshold']) & (df_plot['volume'] >= static_volume_threshold),
color='blue', alpha=0.1, label='Filtered by Rolling Only')
ax.set_title('Comparison of Static vs. Rolling Liquidity Filters')
ax.set_xlabel('Date')
ax.set_ylabel('Volume')
ax.legend()
ax.grid(True, linestyle='--', alpha=0.6)
plt.tight_layout()
plt.show()
print("\nInterpretation:")
print("This plot demonstrates how the rolling adaptive threshold (purple dotted line) dynamically changes based on the recent average volume, unlike the fixed static threshold (green dashed line). You can observe periods where the rolling filter might be more or less permissive than the static one, depending on the current market's historical liquidity levels. The shaded areas highlight where each filter would discard data, showcasing their distinct filtering characteristics. The rolling filter adapts to recent market conditions, which can be advantageous in volatile or trending volume environments.")Interpretation: This plot demonstrates how the rolling adaptive threshold (purple dotted line) dynamically changes based on the recent average volume, unlike the fixed static threshold (green dashed line). You can observe periods where the rolling filter might be more or less permissive than the static one, depending on the current market's historical liquidity levels. The shaded areas highlight where each filter would discard data, showcasing their distinct filtering characteristics. The rolling filter adapts to recent market conditions, which can be advantageous in volatile or trending volume environments.
Conclusion
Liquidity filters are essential tools in quantitative finance and algorithmic trading. They help traders and analysts focus on market conditions where execution risk is manageable and transaction costs are minimized. By ensuring that strategies are only applied to sufficiently liquid instruments, these filters contribute to more robust and reliable trading systems.
We explored two common types of liquidity filters:
- Static Volume Threshold: A simple and effective method for discarding data below a fixed volume level.
- Rolling Average Volume Threshold: A more adaptive approach that adjusts the liquidity criterion based on recent market activity, providing flexibility in varying market conditions.
The choice between a static and a rolling filter depends on the specific requirements of the trading strategy, the characteristics of the asset, and the market environment. Understanding and properly implementing liquidity filters is a critical step towards building sophisticated and practical financial models.