Agent skill
tjr-liquidity-detection
TJR liquidity sweep and Fair Value Gap detection - PDH/PDL sweeps with fast reclaim, FVG midpoint targeting, and volume confirmation for high-probability reversals
Install this agent skill to your Project
npx add-skill https://github.com/Nice-Wolf-Studio/wolf-skills-marketplace/tree/main/tjr-liquidity-detection
SKILL.md
TJR Liquidity Detection
Overview
Liquidity sweep and Fair Value Gap (FVG) detection are core TJR patterns for identifying high-probability reversal points. The classic pattern: price wicks beyond a key level (grabbing stops), then rapidly reverses back.
Core Patterns:
- PDH/PDL Sweeps: Price sweeps Prior Day High/Low, then reclaims quickly
- Fair Value Gaps: 3-candle imbalances that act as rebalance targets
- Volume Confirmation: Volume spike validates true sweep vs fake
- VWAP Alignment: Recapture of VWAP confirms reversal strength
Use this skill when:
- Building TJR-based trading bots for intraday reversals
- Detecting stop hunts and liquidity grabs
- Finding precise entry points after liquidity sweeps
- Targeting fair value gap midpoints for position scaling
Prior Day High/Low (PDH/PDL) Tracking
What is PDH/PDL?
PDH (Prior Day High): The highest price from the previous trading session PDL (Prior Day Low): The lowest price from the previous trading session
These levels act as liquidity magnets where stops cluster. Institutional traders often sweep these levels to grab liquidity before reversing.
TypeScript Implementation:
interface SessionLevels {
date: string;
high: number;
low: number;
open: number;
close: number;
}
function getPriorDayLevels(
candles: Candle[],
currentSessionStart: Date
): SessionLevels | null {
// Filter candles from prior trading session
const priorSessionEnd = new Date(currentSessionStart);
priorSessionEnd.setDate(priorSessionEnd.getDate() - 1);
priorSessionEnd.setHours(16, 0, 0, 0); // 4:00 PM ET close
const priorSessionStart = new Date(priorSessionEnd);
priorSessionStart.setHours(9, 30, 0, 0); // 9:30 AM ET open
const priorCandles = candles.filter(c => {
const candleTime = new Date(c.timestamp);
return candleTime >= priorSessionStart && candleTime < priorSessionEnd;
});
if (priorCandles.length === 0) return null;
return {
date: priorSessionStart.toISOString().split('T')[0],
high: Math.max(...priorCandles.map(c => c.high)),
low: Math.min(...priorCandles.map(c => c.low)),
open: priorCandles[0].open,
close: priorCandles[priorCandles.length - 1].close
};
}
// Track multiple prior sessions for context
function getMultiDayLevels(
candles: Candle[],
currentSessionStart: Date,
daysBack: number = 5
): SessionLevels[] {
const levels: SessionLevels[] = [];
for (let i = 1; i <= daysBack; i++) {
const sessionStart = new Date(currentSessionStart);
sessionStart.setDate(sessionStart.getDate() - i);
const priorLevels = getPriorDayLevels(candles, sessionStart);
if (priorLevels) {
levels.push(priorLevels);
}
}
return levels;
}
Liquidity Sweep Detection
What is a Liquidity Sweep?
A liquidity sweep occurs when price:
- Wicks beyond a key level (PDH, PDL, or swing high/low)
- Triggers stops clustered at that level
- Rapidly reverses back inside the prior range (within 3-5 bars)
Classic Pattern:
PDH Sweep (Bearish):
1. Price wicks above PDH (grabs buy-side liquidity)
2. Immediate rejection (closes back below PDH within 3 bars)
3. Reversal downward (target: mid-range, VWAP, or PDL)
TypeScript Implementation:
interface LiquiditySweep {
timestamp: number;
type: 'pdh_sweep' | 'pdl_sweep' | 'swing_high_sweep' | 'swing_low_sweep';
level: number; // The level that was swept
sweepHigh: number; // How far above level (for bullish sweep)
sweepLow: number; // How far below level (for bearish sweep)
reclaimBar: number; // Index where price reclaimed level
reclaimSpeed: number; // Bars taken to reclaim (lower = faster = better)
volumeSpike: boolean; // Was there volume confirmation?
direction: 'bullish' | 'bearish'; // Expected reversal direction
confidence: number; // 0-1 score
}
function detectPDHSweep(
candles: Candle[],
pdh: number,
params: {
sweepToleranceTicks: number; // e.g., 2 ticks beyond PDH
reclaimBars: number; // e.g., 3-5 bars
volumeZScoreThreshold: number; // e.g., 1.5
}
): LiquiditySweep[] {
const sweeps: LiquiditySweep[] = [];
const tickSize = 0.25; // ES/NQ tick size
for (let i = params.reclaimBars; i < candles.length; i++) {
const candle = candles[i];
// Check if candle wicked above PDH
const wickedAbovePDH = candle.high > pdh + (params.sweepToleranceTicks * tickSize);
if (!wickedAbovePDH) continue;
// Check if price reclaimed (closed back below PDH) within N bars
let reclaimBar = -1;
for (let j = i; j < Math.min(i + params.reclaimBars, candles.length); j++) {
if (candles[j].close < pdh) {
reclaimBar = j;
break;
}
}
if (reclaimBar === -1) continue; // No reclaim
// Check volume confirmation
const recentCandles = candles.slice(Math.max(0, i - 20), i);
const avgVolume = recentCandles.reduce((sum, c) => sum + c.volume, 0) / recentCandles.length;
const stdVolume = standardDeviation(recentCandles.map(c => c.volume));
const volumeZScore = (candle.volume - avgVolume) / (stdVolume || 1);
const volumeSpike = volumeZScore >= params.volumeZScoreThreshold;
sweeps.push({
timestamp: candle.timestamp,
type: 'pdh_sweep',
level: pdh,
sweepHigh: candle.high,
sweepLow: candle.low,
reclaimBar,
reclaimSpeed: reclaimBar - i,
volumeSpike,
direction: 'bearish', // PDH sweep is bearish
confidence: calculateSweepConfidence(reclaimBar - i, volumeSpike)
});
}
return sweeps;
}
function detectPDLSweep(
candles: Candle[],
pdl: number,
params: {
sweepToleranceTicks: number;
reclaimBars: number;
volumeZScoreThreshold: number;
}
): LiquiditySweep[] {
const sweeps: LiquiditySweep[] = [];
const tickSize = 0.25;
for (let i = params.reclaimBars; i < candles.length; i++) {
const candle = candles[i];
// Check if candle wicked below PDL
const wickedBelowPDL = candle.low < pdl - (params.sweepToleranceTicks * tickSize);
if (!wickedBelowPDL) continue;
// Check if price reclaimed (closed back above PDL) within N bars
let reclaimBar = -1;
for (let j = i; j < Math.min(i + params.reclaimBars, candles.length); j++) {
if (candles[j].close > pdl) {
reclaimBar = j;
break;
}
}
if (reclaimBar === -1) continue;
// Volume confirmation
const recentCandles = candles.slice(Math.max(0, i - 20), i);
const avgVolume = recentCandles.reduce((sum, c) => sum + c.volume, 0) / recentCandles.length;
const stdVolume = standardDeviation(recentCandles.map(c => c.volume));
const volumeZScore = (candle.volume - avgVolume) / (stdVolume || 1);
const volumeSpike = volumeZScore >= params.volumeZScoreThreshold;
sweeps.push({
timestamp: candle.timestamp,
type: 'pdl_sweep',
level: pdl,
sweepHigh: candle.high,
sweepLow: candle.low,
reclaimBar,
reclaimSpeed: reclaimBar - i,
volumeSpike,
direction: 'bullish', // PDL sweep is bullish
confidence: calculateSweepConfidence(reclaimBar - i, volumeSpike)
});
}
return sweeps;
}
function calculateSweepConfidence(reclaimSpeed: number, volumeSpike: boolean): number {
let confidence = 0.5; // Base
// Faster reclaim = higher confidence
if (reclaimSpeed <= 1) confidence += 0.3; // Immediate reclaim
else if (reclaimSpeed <= 3) confidence += 0.2;
else if (reclaimSpeed <= 5) confidence += 0.1;
// Volume spike adds confidence
if (volumeSpike) confidence += 0.2;
return Math.min(confidence, 1.0);
}
Fair Value Gap (FVG) Detection
What is a Fair Value Gap?
FVG is a 3-candle imbalance pattern where price moves so quickly that it leaves a "gap" - a price range with minimal trading activity.
Detection:
Bullish FVG: candle[i-1].low > candle[i+1].high
Bearish FVG: candle[i-1].high < candle[i+1].low
Midpoint Targeting: The midpoint of the FVG is the primary rebalance target.
TypeScript Implementation (adapted from ICT skill):
interface FairValueGap {
type: 'bullish' | 'bearish';
startIndex: number;
gapHigh: number;
gapLow: number;
midpoint: number;
filled: boolean;
filledAtIndex?: number;
}
function detectFairValueGaps(candles: Candle[]): FairValueGap[] {
const fvgs: FairValueGap[] = [];
for (let i = 1; i < candles.length - 1; i++) {
const prev = candles[i - 1];
const current = candles[i];
const next = candles[i + 1];
// Bullish FVG
if (prev.low > next.high) {
fvgs.push({
type: 'bullish',
startIndex: i,
gapHigh: prev.low,
gapLow: next.high,
midpoint: (prev.low + next.high) / 2,
filled: false
});
}
// Bearish FVG
if (prev.high < next.low) {
fvgs.push({
type: 'bearish',
startIndex: i,
gapHigh: next.low,
gapLow: prev.high,
midpoint: (next.low + prev.high) / 2,
filled: false
});
}
}
return fvgs;
}
// Check if FVG has been filled
function updateFVGStatus(fvgs: FairValueGap[], candles: Candle[]): void {
fvgs.forEach(fvg => {
if (fvg.filled) return; // Already filled
const candlesAfterGap = candles.slice(fvg.startIndex + 1);
for (let i = 0; i < candlesAfterGap.length; i++) {
const candle = candlesAfterGap[i];
// Bullish FVG filled if price drops to midpoint
if (fvg.type === 'bullish' && candle.low <= fvg.midpoint) {
fvg.filled = true;
fvg.filledAtIndex = fvg.startIndex + 1 + i;
break;
}
// Bearish FVG filled if price rises to midpoint
if (fvg.type === 'bearish' && candle.high >= fvg.midpoint) {
fvg.filled = true;
fvg.filledAtIndex = fvg.startIndex + 1 + i;
break;
}
}
});
}
// Find nearest unfilled FVG for entry targeting
function findNearestFVG(
fvgs: FairValueGap[],
currentPrice: number,
direction: 'bullish' | 'bearish'
): FairValueGap | null {
const unfilledFVGs = fvgs.filter(f => !f.filled && f.type === direction);
if (unfilledFVGs.length === 0) return null;
// Find FVG closest to current price
let nearest = unfilledFVGs[0];
let minDistance = Math.abs(currentPrice - nearest.midpoint);
for (const fvg of unfilledFVGs) {
const distance = Math.abs(currentPrice - fvg.midpoint);
if (distance < minDistance) {
minDistance = distance;
nearest = fvg;
}
}
return nearest;
}
Debounce Logic
Why Debounce?
Prevent generating multiple signals for the same liquidity level within a short time window. If price sweeps PDH at 9:35, don't signal again at 9:36 for the same level.
TypeScript Implementation:
interface DebounceState {
level: number;
lastSignalBar: number;
debounceWindow: number; // e.g., 10 bars
}
class SweepDebouncer {
private state: Map<string, DebounceState> = new Map();
shouldSignal(
level: number,
currentBar: number,
debounceWindow: number = 10,
tolerance: number = 0.5 // Tick tolerance
): boolean {
const key = this.getLevelKey(level, tolerance);
const existing = this.state.get(key);
if (!existing) {
// First time seeing this level
this.state.set(key, { level, lastSignalBar: currentBar, debounceWindow });
return true;
}
// Check if outside debounce window
if (currentBar - existing.lastSignalBar >= debounceWindow) {
// Update and allow signal
existing.lastSignalBar = currentBar;
return true;
}
// Still in debounce window, block signal
return false;
}
private getLevelKey(level: number, tolerance: number): string {
// Round to tolerance to group nearby levels
const rounded = Math.round(level / tolerance) * tolerance;
return rounded.toFixed(2);
}
reset(): void {
this.state.clear();
}
}
// Usage in sweep detection
const debouncer = new SweepDebouncer();
function detectSweepsWithDebounce(
candles: Candle[],
pdh: number,
pdl: number
): LiquiditySweep[] {
const allSweeps: LiquiditySweep[] = [];
// Detect PDH sweeps
const pdhSweeps = detectPDHSweep(candles, pdh, {
sweepToleranceTicks: 2,
reclaimBars: 5,
volumeZScoreThreshold: 1.5
});
// Detect PDL sweeps
const pdlSweeps = detectPDLSweep(candles, pdl, {
sweepToleranceTicks: 2,
reclaimBars: 5,
volumeZScoreThreshold: 1.5
});
// Combine and filter with debounce
[...pdhSweeps, ...pdlSweeps].forEach(sweep => {
const sweepBarIndex = candles.findIndex(c => c.timestamp === sweep.timestamp);
if (debouncer.shouldSignal(sweep.level, sweepBarIndex, 10)) {
allSweeps.push(sweep);
}
});
return allSweeps;
}
VWAP Integration
Why VWAP Matters
VWAP (Volume Weighted Average Price) acts as a fair value anchor. When price sweeps a level and reclaims VWAP, it adds significant confluence.
Recapture Pattern:
1. Price sweeps PDL (bullish setup)
2. Price reclaims PDL quickly
3. Price also reclaims VWAP → STRONG BULLISH CONFIRMATION
TypeScript Implementation:
function calculateSessionVWAP(candles: Candle[], sessionStart: Date): number[] {
const vwaps: number[] = [];
let cumulativePV = 0; // Price * Volume
let cumulativeVolume = 0;
const sessionCandles = candles.filter(c => new Date(c.timestamp) >= sessionStart);
sessionCandles.forEach(candle => {
const typicalPrice = (candle.high + candle.low + candle.close) / 3;
cumulativePV += typicalPrice * candle.volume;
cumulativeVolume += candle.volume;
vwaps.push(cumulativeVolume > 0 ? cumulativePV / cumulativeVolume : typicalPrice);
});
return vwaps;
}
function checkVWAPRecapture(
sweep: LiquiditySweep,
candles: Candle[],
vwaps: number[]
): boolean {
if (!sweep.reclaimBar || sweep.reclaimBar >= vwaps.length) return false;
const sweepIndex = candles.findIndex(c => c.timestamp === sweep.timestamp);
const reclaimIndex = sweep.reclaimBar;
const vwapAtSweep = vwaps[sweepIndex];
const vwapAtReclaim = vwaps[reclaimIndex];
if (sweep.direction === 'bullish') {
// For bullish sweep, check if price recaptured above VWAP
const sweepWasBelowVWAP = candles[sweepIndex].close < vwapAtSweep;
const reclaimIsAboveVWAP = candles[reclaimIndex].close > vwapAtReclaim;
return sweepWasBelowVWAP && reclaimIsAboveVWAP;
} else {
// For bearish sweep, check if price recaptured below VWAP
const sweepWasAboveVWAP = candles[sweepIndex].close > vwapAtSweep;
const reclaimIsBelowVWAP = candles[reclaimIndex].close < vwapAtReclaim;
return sweepWasAboveVWAP && reclaimIsBelowVWAP;
}
}
Complete Liquidity Signal Generation
TypeScript Implementation:
interface LiquiditySignal {
timestamp: number;
type: 'pdh_sweep_short' | 'pdl_sweep_long';
sweep: LiquiditySweep;
entry: number; // Suggested entry (current price or FVG midpoint)
target: number; // Target (opposite PDH/PDL, mid-range, etc.)
stopLoss: number; // Invalidation level
fvg?: FairValueGap; // Associated FVG if available
vwapRecapture: boolean;
confidence: number;
reasoning: string[];
}
async function generateLiquiditySignals(
candles: Candle[],
sessionStart: Date
): Promise<LiquiditySignal[]> {
const signals: LiquiditySignal[] = [];
// 1. Get prior day levels
const priorLevels = getPriorDayLevels(candles, sessionStart);
if (!priorLevels) return signals;
// 2. Detect sweeps
const sweeps = detectSweepsWithDebounce(candles, priorLevels.high, priorLevels.low);
// 3. Detect FVGs
const fvgs = detectFairValueGaps(candles);
updateFVGStatus(fvgs, candles);
// 4. Calculate VWAP
const vwaps = calculateSessionVWAP(candles, sessionStart);
// 5. Convert sweeps to trading signals
for (const sweep of sweeps) {
const currentPrice = candles[candles.length - 1].close;
const vwapRecapture = checkVWAPRecapture(sweep, candles, vwaps);
// Find associated FVG for entry
const associatedFVG = findNearestFVG(
fvgs,
sweep.level,
sweep.direction
);
let entry = currentPrice;
if (associatedFVG && !associatedFVG.filled) {
entry = associatedFVG.midpoint; // Use FVG midpoint as entry
}
// Calculate targets
let target: number;
let stopLoss: number;
if (sweep.direction === 'bullish') {
// PDL sweep long: target PDH, stop below PDL
target = priorLevels.high;
stopLoss = priorLevels.low - 2; // 2 points below PDL
} else {
// PDH sweep short: target PDL, stop above PDH
target = priorLevels.low;
stopLoss = priorLevels.high + 2; // 2 points above PDH
}
// Calculate confidence
let confidence = sweep.confidence;
if (vwapRecapture) confidence = Math.min(confidence + 0.15, 1.0);
if (associatedFVG) confidence = Math.min(confidence + 0.1, 1.0);
signals.push({
timestamp: sweep.timestamp,
type: sweep.direction === 'bullish' ? 'pdl_sweep_long' : 'pdh_sweep_short',
sweep,
entry,
target,
stopLoss,
fvg: associatedFVG || undefined,
vwapRecapture,
confidence,
reasoning: [
`${sweep.type} detected at ${sweep.level.toFixed(2)}`,
`Reclaimed in ${sweep.reclaimSpeed} bar(s)`,
sweep.volumeSpike ? 'Volume spike confirmed' : 'No volume spike',
vwapRecapture ? 'VWAP recapture confirmed' : 'No VWAP recapture',
associatedFVG ? `FVG entry at ${associatedFVG.midpoint.toFixed(2)}` : 'No FVG entry',
`Target: ${target.toFixed(2)}, Stop: ${stopLoss.toFixed(2)}`
]
});
}
return signals;
}
Integration with Existing Skills
Reusing ICT Strategy
Fair Value Gap Logic:
// Import FVG detection from ict-strategy
import { detectFairValueGaps, updateFVGStatus } from '../ict-strategy';
// Use directly in TJR liquidity detection
const fvgs = detectFairValueGaps(candles);
updateFVGStatus(fvgs, candles);
Reusing Trading-Foundations
Volume Z-Score:
// Import from trading-foundations
import { standardDeviation } from '../trading-foundations';
// Use for volume confirmation
const volumeZScore = (candle.volume - avgVolume) / standardDeviation(volumes);
VWAP Calculation:
// VWAP already covered in trading-foundations - reuse that implementation
Discord Bot Integration
Example Command:
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
const liquidityCommand = new SlashCommandBuilder()
.setName('liquidity-sweeps')
.setDescription('Detect liquidity sweeps and FVG patterns')
.addStringOption(option =>
option.setName('symbol')
.setDescription('Symbol (ES or NQ)')
.setRequired(true)
.addChoices(
{ name: 'ES', value: 'ES' },
{ name: 'NQ', value: 'NQ' }
));
async function handleLiquidityCommand(interaction) {
const symbol = interaction.options.getString('symbol');
await interaction.deferReply();
try {
const sessionStart = getTodaySessionStart();
const candles = await dataManager.getCandles(symbol, '5m', 200);
const signals = await generateLiquiditySignals(candles, sessionStart);
if (signals.length === 0) {
await interaction.editReply('No liquidity sweeps detected.');
return;
}
const latest = signals[signals.length - 1];
const embed = new EmbedBuilder()
.setTitle(`💧 Liquidity Sweep: ${symbol}`)
.setColor(latest.sweep.direction === 'bullish' ? 0x00FF00 : 0xFF0000)
.addFields([
{
name: 'Type',
value: latest.type.toUpperCase().replace(/_/g, ' '),
inline: false
},
{
name: 'Level Swept',
value: latest.sweep.level.toFixed(2),
inline: true
},
{
name: 'Reclaim Speed',
value: `${latest.sweep.reclaimSpeed} bars`,
inline: true
},
{
name: 'Confidence',
value: `${(latest.confidence * 100).toFixed(0)}%`,
inline: true
},
{
name: 'Entry',
value: latest.entry.toFixed(2),
inline: true
},
{
name: 'Target',
value: latest.target.toFixed(2),
inline: true
},
{
name: 'Stop Loss',
value: latest.stopLoss.toFixed(2),
inline: true
},
{
name: 'Confirmations',
value: [
latest.sweep.volumeSpike ? '✅ Volume spike' : '❌ No volume spike',
latest.vwapRecapture ? '✅ VWAP recapture' : '❌ No VWAP recapture',
latest.fvg ? '✅ FVG entry available' : '❌ No FVG'
].join('\n'),
inline: false
},
{
name: 'Analysis',
value: latest.reasoning.join('\n'),
inline: false
}
])
.setTimestamp()
.setFooter({ text: `${signals.length} sweep(s) detected today` });
await interaction.editReply({ embeds: [embed] });
} catch (error) {
await interaction.editReply(`Error: ${error.message}`);
}
}
Common Pitfalls
1. No Volume Confirmation
- ❌ Taking every sweep without checking volume
- ✅ Filter for volume z-score ≥ 1.5
2. Slow Reclaims
- ❌ Accepting reclaims that take 10+ bars
- ✅ Prefer reclaims within 3-5 bars max
3. Ignoring VWAP
- ❌ Not checking VWAP recapture
- ✅ Add VWAP confluence for higher probability
4. No Debounce
- ❌ Signaling same level repeatedly
- ✅ Use 10-bar debounce window
5. Wrong Targets
- ❌ Using arbitrary targets
- ✅ Target opposite PDH/PDL, FVG midpoints, or session mid-range
After Using This Skill
Next steps:
- Use
tjr-smt-divergencefor directional bias confirmation - Use
tjr-session-patternsfor multi-timeframe confidence - Use
tjr-multi-timeframe-confluenceto combine all TJR signals
For backtesting:
- Test PDH vs PDL sweep win rates separately
- Measure impact of volume filter
- Track VWAP recapture lift (how much better are signals with VWAP?)
Last Updated: January 2025 Version: 1.0.0 Part of Wolf Skills Marketplace - TJR Series
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
daily-summary
Use when preparing daily standups or status reports - automates PR summary generation with categorization, metrics, and velocity analysis; eliminates manual report compilation and ensures consistent format
wolf-scripts-core
Core automation scripts for archetype selection, evidence validation, quality scoring, and safe bash execution
wolf
Master skill for Wolf Agents institutional knowledge and behavioral patterns (v1.1.0 with skill-chaining)
wolf-archetypes
Behavioral archetypes for automatic agent adaptation based on work type
ict-strategy
Inner Circle Trader (ICT) methodology - smart money concepts, liquidity pools, fair value gaps, order blocks, and market structure for trading bot implementation
databento
Use when working with ES/NQ futures market data, before calling any Databento API - follow mandatory four-step workflow (cost check, availability check, fetch, validate); prevents costly API errors and ensures data quality
Didn't find tool you were looking for?