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

Stars 2
Forks 0

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:

  1. PDH/PDL Sweeps: Price sweeps Prior Day High/Low, then reclaims quickly
  2. Fair Value Gaps: 3-candle imbalances that act as rebalance targets
  3. Volume Confirmation: Volume spike validates true sweep vs fake
  4. 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:

typescript
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:

  1. Wicks beyond a key level (PDH, PDL, or swing high/low)
  2. Triggers stops clustered at that level
  3. 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:

typescript
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):

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
// 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:

typescript
// Import from trading-foundations
import { standardDeviation } from '../trading-foundations';

// Use for volume confirmation
const volumeZScore = (candle.volume - avgVolume) / standardDeviation(volumes);

VWAP Calculation:

typescript
// VWAP already covered in trading-foundations - reuse that implementation

Discord Bot Integration

Example Command:

typescript
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:

  1. Use tjr-smt-divergence for directional bias confirmation
  2. Use tjr-session-patterns for multi-timeframe confidence
  3. Use tjr-multi-timeframe-confluence to 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

Expand your agent's capabilities with these related and highly-rated skills.

Didn't find tool you were looking for?

Be as detailed as possible for better results