Liquidity Volume Signal Confirmation
Confirm all trading signals using real-time volume profile analysis and order book liquidity assessment, requiring sufficient market depth and participation before execution to avoid excessive slippage in thin or illiquid market conditions.
Liquidity and Volume Signal Confirmation
This notebook outlines a methodology for validating directional trading signals by incorporating volume and price spread characteristics. The objective is to confirm signals only when supported by significant market activity and price movement.
Setup
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplotsData Preparation
A sample OHLCV (Open, High, Low, Close, Volume) DataFrame is generated for demonstration purposes. This DataFrame simulates typical financial time series data necessary for the signal confirmation model.
# Generate synthetic OHLCV data for demonstration
dates = pd.to_datetime(pd.date_range(start='2023-01-01', periods=100, freq='h'))
np.random.seed(42)
open_prices = 100 + np.cumsum(np.random.randn(100))
high_prices = open_prices + np.random.rand(100) * 2
low_prices = open_prices - np.random.rand(100) * 2
close_prices = open_prices + (np.random.rand(100) - 0.5) * 3
volume = np.random.randint(1000, 10000, 100)
df = pd.DataFrame({
'datetime': dates,
'open': open_prices,
'high': high_prices,
'low': low_prices,
'close': close_prices,
'volume': volume
})
display(df.head())| datetime | open | high | low | close | volume | |
|---|---|---|---|---|---|---|
| 0 | 2023-01-01 00:00:00 | 100.496714 | 101.331536 | 98.907092 | 100.064632 | 7731 |
| 1 | 2023-01-01 01:00:00 | 100.358450 | 100.802665 | 99.353176 | 101.131988 | 8241 |
| 2 | 2023-01-01 02:00:00 | 101.006138 | 101.245869 | 99.852331 | 99.549319 | 1953 |
| 3 | 2023-01-01 03:00:00 | 102.529168 | 103.204399 | 101.544133 | 101.377386 | 3539 |
| 4 | 2023-01-01 04:00:00 | 102.295015 | 104.180834 | 101.904529 | 100.933023 | 8056 |
Signal Confirmation Model
The liquidity_volume_signal_confirmation function confirms EMA cross signals using three independent criteria: volume ratio, bar range relative to Average True Range (ATR), and On-Balance Volume (OBV) trend. A base EMA cross signal is confirmed only when all three criteria are met, ensuring robustness.
def liquidity_volume_signal_confirmation(
df: pd.DataFrame,
ema_fast: int = 12,
ema_slow: int = 26,
vol_window: int = 20,
vol_factor: float = 1.3,
atr_period: int = 14,
range_factor: float = 1.0,
) -> pd.DataFrame:
"""
Confirms EMA cross signals using volume, ATR-normalised range, and OBV trend.
Core Logic:
1. Computes an EMA cross base signal.
2. Computes three independent confirmation metrics:
- Volume ratio against a rolling average.
- Bar range against Average True Range (ATR).
- On-Balance Volume (OBV) trend against its Simple Moving Average (SMA).
3. Emits a final signal exclusively when all three confirmation criteria align
with the EMA cross direction.
Parameters:
-----------
df : pd.DataFrame
OHLCV DataFrame containing 'datetime', 'open', 'high', 'low', 'close', and 'volume'.
ema_fast : int
Period for the fast Exponential Moving Average.
ema_slow : int
Period for the slow Exponential Moving Average.
vol_window : int
Rolling window for the volume average calculation.
vol_factor : float
Minimum volume ratio required for confirmation.
atr_period : int
Period for the Average True Range (ATR) calculation.
range_factor : float
Minimum bar range / ATR ratio required for confirmation.
Returns:
--------
pd.DataFrame
Original DataFrame with added columns: `ema_fast`, `ema_slow`,
`ema_signal`, `avg_vol`, `vol_ratio`, `vol_confirm`, `atr`,
`bar_range`, `range_confirm`, `obv`, `obv_sma`, `obv_confirm`,
and `signal`.
"""
df = df.copy().sort_values("datetime", ignore_index=True)
# ── EMA Cross Signal ──────────────────────────────────────────────────────
df["ema_fast"] = df["close"].ewm(span=ema_fast, adjust=False).mean()
df["ema_slow"] = df["close"].ewm(span=ema_slow, adjust=False).mean()
df["ema_signal"] = np.where(df["ema_fast"] > df["ema_slow"], 1, -1)
# ── Volume Confirmation ───────────────────────────────────────────────────
df["avg_vol"] = df["volume"].rolling(vol_window).mean()
df["vol_ratio"] = df["volume"] / df["avg_vol"].replace(0, np.nan)
df["vol_confirm"] = df["vol_ratio"] > vol_factor
# ── ATR and Range Confirmation ────────────────────────────────────────────
tr = pd.concat([
df["high"] - df["low"],
(df["high"] - df["close"].shift(1)).abs(),
(df["low"] - df["close"].shift(1)).abs(),
], axis=1).max(axis=1)
df["atr"] = tr.rolling(atr_period).mean()
df["bar_range"] = df["high"] - df["low"]
df["range_confirm"] = df["bar_range"] > range_factor * df["atr"]
# ── OBV Trend Confirmation ────────────────────────────────────────────────
obv_direction = np.where(df["close"] > df["close"].shift(1), 1, -1)
df["obv"] = (obv_direction * df["volume"]).cumsum()
df["obv_sma"] = df["obv"].rolling(20).mean()
df["obv_confirm"] = np.where(
(df["ema_signal"] == 1) & (df["obv"] > df["obv_sma"]), True,
np.where((df["ema_signal"] == -1) & (df["obv"] < df["obv_sma"]), True, False)
)
# ── Final Confirmed Signal ────────────────────────────────────────────────
df["signal"] = np.where(
df["vol_confirm"] & df["range_confirm"] & df["obv_confirm"],
df["ema_signal"], 0
)
return dfSignal Generation
The liquidity_volume_signal_confirmation function is applied to the prepared DataFrame to generate confirmed buy (1) or sell (-1) signals, or no signal (0).
df_signals = liquidity_volume_signal_confirmation(df)
print("Signal Count Distribution:")
print(df_signals["signal"].value_counts())Signal Count Distribution: signal 0 91 -1 8 1 1 Name: count, dtype: int64
Key Model Components
- OBV (On-Balance Volume): A cumulative sum of signed volume. A rising OBV indicates accumulation (bullish sentiment), while a falling OBV suggests distribution (bearish sentiment).
- All-Three Requirement: The logical AND condition (
vol_confirm & range_confirm & obv_confirm) ensures that only high-quality signals, supported by significant volume, favorable spread characteristics, and a confirmed OBV trend, are generated. This filtering mechanism aims to capture robust, institutionally-backed price movements.
Limitation: The specified spread and volume thresholds (vol_factor and range_factor) may inadvertently filter out valid signals in instruments characterized by low liquidity. Consequently, these thresholds require instrument-specific calibration to maintain signal efficacy.
Signal Visualization
The generated signals are visualized alongside price action, volume, and OBV to provide a comprehensive view of the model's output. Confirmed buy and sell signals are overlaid on the candlestick chart, while volume and OBV trends are displayed in separate subplots.
buy_signals = df_signals[df_signals["signal"] == 1]
sell_signals = df_signals[df_signals["signal"] == -1]
fig = make_subplots(rows=3, cols=1, shared_xaxes=True,
subplot_titles=["Price + Confirmed Signals", "Volume", "OBV"],
row_heights=[0.5, 0.25, 0.25])
fig.add_trace(go.Candlestick(x=df_signals["datetime"],
open=df_signals["open"], high=df_signals["high"],
low=df_signals["low"], close=df_signals["close"], name="Price"), row=1, col=1)
fig.add_trace(go.Scatter(x=buy_signals["datetime"], y=buy_signals["low"] * 0.999,
mode="markers", marker=dict(symbol="triangle-up", size=10, color="green"), name="Buy"), row=1, col=1)
fig.add_trace(go.Scatter(x=sell_signals["datetime"], y=sell_signals["high"] * 1.001,
mode="markers", marker=dict(symbol="triangle-down", size=10, color="red"), name="Sell"), row=1, col=1)
fig.add_trace(go.Bar(x=df_signals["datetime"], y=df_signals["volume"], name="Volume",
marker_color=["green" if c else "gray" for c in df_signals["vol_confirm"]]), row=2, col=1)
fig.add_trace(go.Scatter(x=df_signals["datetime"], y=df_signals["obv"],
mode="lines", name="OBV", line=dict(color="blue")), row=3, col=1)
fig.add_trace(go.Scatter(x=df_signals["datetime"], y=df_signals["obv_sma"],
mode="lines", name="OBV SMA", line=dict(color="orange", dash="dash")), row=3, col=1)
fig.update_layout(title_text="Liquidity + Volume Signal Confirmation",
xaxis_rangeslider_visible=False, height=800, xaxis3_title="Datetime")
fig.show()Conclusion
This notebook presented a methodology for confirming directional trading signals by integrating liquidity and volume characteristics. The liquidity_volume_signal_confirmation function was developed to confirm EMA cross signals only when supported by three independent criteria:
- Volume Ratio: Current volume significantly exceeds its rolling average, indicating strong market participation.
- Bar Range vs. ATR: The current bar's range is substantial relative to the Average True Range (ATR), suggesting meaningful price movement.
- OBV Trend: On-Balance Volume (OBV) trend confirms the direction of the EMA cross, indicating accumulation for bullish signals and distribution for bearish signals.
This "all-three" confirmation approach aims to filter out weak signals and focus on high-conviction trading opportunities, potentially backed by institutional activity. While effective in theory, it's crucial to acknowledge the limitation that volume and spread thresholds (vol_factor and range_factor) require instrument-specific calibration to maintain signal efficacy, especially in low-liquidity markets. The visualization further demonstrated how these confirmed signals align with price action, volume, and OBV trends.