Agent skill
tjr-session-patterns
TJR (The Jared Ryan) session pattern recognition and multi-timeframe clustering. Use when implementing sweep clustering, session pattern detection (London flat → NY reversal), or multi-timeframe confidence analysis for ES/NQ futures trading bots. Reuses: trading-foundations, tjr-liquidity-detection, tjr-smt-divergence
Install this agent skill to your Project
npx add-skill https://github.com/Nice-Wolf-Studio/wolf-skills-marketplace/tree/main/tjr-session-patterns
SKILL.md
TJR Session Patterns
Overview
This skill provides reference material for implementing TJR's session pattern recognition and multi-timeframe clustering analysis. It focuses on:
- Sweep Clustering: Grouping PDH/PDL sweeps across time and price tolerance windows
- Session Patterns: Detecting London flat → NY reversal, steep selloff → bounce patterns
- Multi-timeframe Ladder: Building confidence from 1-minute → 1-hour → daily alignment
- Accumulation/Distribution: Identifying institutional behavior via clustering density
- Confidence Scoring: Weighted system for pattern strength (50% base + bonuses)
When to use this skill: When coding Discord bots that need to recognize session-based trading patterns, cluster liquidity events, or validate setups across multiple timeframes for ES and NQ futures.
Prerequisites
Before using this skill, you should be familiar with:
- trading-foundations: Candle data models, swing point detection, timeframe resampling
- tjr-liquidity-detection: PDH/PDL sweep detection, FVG patterns, VWAP validation
- tjr-smt-divergence: ES/NQ correlation analysis, structure divergence detection
Key Concepts
Session Structure
TJR methodology focuses on three key sessions:
-
Asian Session (6:00 PM - 2:00 AM ET)
- Range-bound, low liquidity
- Sets up sweep levels for later sessions
- Not primary trading window
-
London Session (3:00 AM - 12:00 PM ET)
- First institutional volume
- Creates fake breakouts and range patterns
- "London flat" pattern = accumulation phase
-
New York Session (9:30 AM - 4:00 PM ET)
- Primary trading window (especially 9:30-10:30 AM)
- High-probability reversals after London setups
- Where TJR entries primarily occur
Sweep Clustering
Clustering Window Parameters:
- Time Tolerance: 15 minutes (sweeps occurring within this window cluster)
- Price Tolerance: 0.15% (sweeps at similar price levels cluster)
- Minimum Cluster Size: 2 sweeps (triggers pattern recognition)
- Maximum Cluster Size: No limit (more = stronger signal)
Cluster Types:
interface SweepCluster {
sweeps: LiquiditySweep[];
centerTime: Date;
centerPrice: number;
timeSpan: number; // minutes
priceSpan: number; // points
direction: 'bullish' | 'bearish';
density: number; // sweeps per minute
session: 'london' | 'newyork' | 'asian';
}
Density Interpretation:
- High density (≥ 0.3 sweeps/min): Accumulation/distribution phase
- Medium density (0.1-0.3): Normal liquidity hunting
- Low density (< 0.1): Isolated sweep events
Session Pattern Recognition
Pattern 1: London Flat → NY Reversal
London Session (3 AM - 9:30 AM):
- Price remains within tight range (< 0.5% of session open)
- Multiple failed breakout attempts (sweeps cluster above/below)
- Volume declines throughout session
NY Open (9:30 AM - 10:30 AM):
- Initial sweep in continuation direction
- Fast reclaim (3-5 bars) back into London range
- SMT divergence confirmation (ES vs NQ)
- Reversal trade setup
Pattern 2: Steep Selloff → Bounce
Pre-Market or Early Session:
- Rapid decline (> 1% in < 30 minutes)
- PDL sweep with volume spike
- Multiple sweep clusters at key levels
Bounce Setup:
- Fast reclaim of PDL
- FVG formation during selloff
- Price returns to FVG (50-75% fill)
- Entry at FVG with stop below PDL
Pattern 3: Accumulation Cluster
Extended Time Window (2-4 hours):
- High-density sweep clustering (≥ 0.3 sweeps/min)
- Both PDH and PDL sweeps within same session
- Range contraction (decreasing ATR)
- No clear directional bias
Resolution:
- Breakout direction opposite last sweep cluster
- High probability when aligned with daily/hourly bias
Multi-Timeframe Confidence Ladder
TJR uses a hierarchical confidence system:
Level 1: 1-Minute (Base Layer)
- Weight: 20%
- Detects: Immediate sweep patterns, FVGs, entry precision
- Fast signals, higher noise
Level 2: 15-Minute (Intermediate Layer)
- Weight: 30%
- Detects: Session structure, swing point breaks, sweep clusters
- Balances speed and reliability
Level 3: 1-Hour (Trend Layer)
- Weight: 30%
- Detects: Daily bias, major structure breaks, trend alignment
- Slower signals, higher reliability
Level 4: Daily (Context Layer)
- Weight: 20%
- Detects: Weekly bias, major liquidity levels, macro context
- Context only, not entry timing
Confidence Calculation:
interface MultitimeframeConfidence {
base: number; // 50% starting point
m1Alignment: number; // +10% if 1m supports
m15Alignment: number; // +15% if 15m supports
h1Alignment: number; // +15% if 1h supports
dailyAlignment: number; // +10% if daily supports
total: number; // Sum of all components
}
function calculateConfidence(
m1Signal: SessionPattern | null,
m15Signal: SessionPattern | null,
h1Signal: SessionPattern | null,
dailyBias: 'bullish' | 'bearish' | 'neutral',
primaryDirection: 'bullish' | 'bearish'
): MultitimeframeConfidence {
let confidence: MultitimeframeConfidence = {
base: 50,
m1Alignment: 0,
m15Alignment: 0,
h1Alignment: 0,
dailyAlignment: 0,
total: 50,
};
// Check 1-minute alignment
if (m1Signal && m1Signal.direction === primaryDirection) {
confidence.m1Alignment = 10;
}
// Check 15-minute alignment
if (m15Signal && m15Signal.direction === primaryDirection) {
confidence.m15Alignment = 15;
}
// Check 1-hour alignment
if (h1Signal && h1Signal.direction === primaryDirection) {
confidence.h1Alignment = 15;
}
// Check daily bias alignment
if (dailyBias === primaryDirection) {
confidence.dailyAlignment = 10;
}
confidence.total =
confidence.base +
confidence.m1Alignment +
confidence.m15Alignment +
confidence.h1Alignment +
confidence.dailyAlignment;
return confidence;
}
Implementation Patterns
1. Sweep Clustering Algorithm
import { LiquiditySweep } from '../tjr-liquidity-detection/SKILL.md';
interface ClusterParams {
timeToleranceMinutes: number; // Default: 15
priceTolerancePercent: number; // Default: 0.15
minClusterSize: number; // Default: 2
}
function clusterSweeps(
sweeps: LiquiditySweep[],
params: ClusterParams = {
timeToleranceMinutes: 15,
priceTolerancePercent: 0.15,
minClusterSize: 2,
}
): SweepCluster[] {
const clusters: SweepCluster[] = [];
const processed = new Set<number>();
sweeps.forEach((sweep, i) => {
if (processed.has(i)) return;
const cluster: LiquiditySweep[] = [sweep];
processed.add(i);
// Find all sweeps within tolerance window
sweeps.forEach((other, j) => {
if (i === j || processed.has(j)) return;
const timeDiff = Math.abs(other.timestamp - sweep.timestamp) / (1000 * 60);
const priceDiff = Math.abs(other.level - sweep.level) / sweep.level;
if (
timeDiff <= params.timeToleranceMinutes &&
priceDiff <= params.priceTolerancePercent / 100 &&
other.direction === sweep.direction
) {
cluster.push(other);
processed.add(j);
}
});
// Only create cluster if meets minimum size
if (cluster.length >= params.minClusterSize) {
const centerTime = new Date(
cluster.reduce((sum, s) => sum + s.timestamp, 0) / cluster.length
);
const centerPrice =
cluster.reduce((sum, s) => sum + s.level, 0) / cluster.length;
const timestamps = cluster.map(s => s.timestamp);
const timeSpan = (Math.max(...timestamps) - Math.min(...timestamps)) / (1000 * 60);
const prices = cluster.map(s => s.level);
const priceSpan = Math.max(...prices) - Math.min(...prices);
const density = cluster.length / Math.max(timeSpan, 1);
const session = getSession(centerTime);
clusters.push({
sweeps: cluster,
centerTime,
centerPrice,
timeSpan,
priceSpan,
direction: sweep.direction,
density,
session,
});
}
});
return clusters.sort((a, b) =>
b.centerTime.getTime() - a.centerTime.getTime()
);
}
function getSession(time: Date): 'london' | 'newyork' | 'asian' {
const hour = time.getUTCHours();
const minute = time.getUTCMinutes();
const totalMinutes = hour * 60 + minute;
// Convert to ET (UTC - 5 during EST, UTC - 4 during EDT)
// Simplified: assume EST for this example
const etMinutes = totalMinutes - 5 * 60;
const normalizedMinutes = ((etMinutes % (24 * 60)) + (24 * 60)) % (24 * 60);
// London: 3:00 AM - 12:00 PM ET = 180 - 720 minutes
if (normalizedMinutes >= 180 && normalizedMinutes < 720) {
return 'london';
}
// New York: 9:30 AM - 4:00 PM ET = 570 - 960 minutes
if (normalizedMinutes >= 570 && normalizedMinutes < 960) {
return 'newyork';
}
// Asian: everything else
return 'asian';
}
2. Session Pattern Detection
interface SessionPattern {
type: 'london_flat_ny_reversal' | 'steep_selloff_bounce' | 'accumulation_cluster';
direction: 'bullish' | 'bearish';
confidence: number;
entry: number;
stopLoss: number;
takeProfit: number;
reasoning: string[];
clusters: SweepCluster[];
timeframe: string;
}
interface SessionData {
open: number;
high: number;
low: number;
close: number;
range: number;
rangePercent: number;
volumeProfile: 'increasing' | 'decreasing' | 'stable';
sweepClusters: SweepCluster[];
}
function detectLondonFlatNYReversal(
londonData: SessionData,
nyCandles: Candle[],
smtDivergence: SMTDivergence | null,
currentPrice: number
): SessionPattern | null {
const reasoning: string[] = [];
// Check London flat condition
if (londonData.rangePercent > 0.5) {
return null; // Not a flat session
}
reasoning.push(`London session flat: ${londonData.rangePercent.toFixed(2)}% range`);
// Check for failed breakout clusters in London
const londonClusters = londonData.sweepClusters.filter(
c => c.session === 'london'
);
if (londonClusters.length === 0) {
return null;
}
reasoning.push(`${londonClusters.length} sweep clusters during London session`);
// Check volume profile (should be decreasing)
if (londonData.volumeProfile !== 'decreasing') {
return null;
}
reasoning.push('London volume profile decreasing (accumulation)');
// Check NY open sweep (first 30 minutes)
const nyOpenEnd = new Date(nyCandles[0].timestamp + 30 * 60 * 1000);
const nyOpenCandles = nyCandles.filter(
c => c.timestamp <= nyOpenEnd.getTime()
);
if (nyOpenCandles.length < 10) {
return null; // Need at least 10 bars (30 minutes of 1-min data)
}
// Detect initial sweep direction
const lastLondonCluster = londonClusters[londonClusters.length - 1];
const sweepDirection = lastLondonCluster.direction;
// Check for fast reclaim (reversal)
const swept = sweepDirection === 'bullish'
? nyOpenCandles.some(c => c.high > londonData.high)
: nyOpenCandles.some(c => c.low < londonData.low);
if (!swept) {
return null; // No sweep occurred
}
const sweepIndex = nyOpenCandles.findIndex(c =>
sweepDirection === 'bullish'
? c.high > londonData.high
: c.low < londonData.low
);
// Check for fast reclaim (3-5 bars)
const reclaimIndex = nyOpenCandles.slice(sweepIndex).findIndex(c =>
sweepDirection === 'bullish'
? c.close < londonData.high - londonData.range * 0.25
: c.close > londonData.low + londonData.range * 0.25
);
if (reclaimIndex === -1 || reclaimIndex > 5) {
return null; // No fast reclaim
}
reasoning.push(`Fast reclaim in ${reclaimIndex} bars after initial sweep`);
// Check SMT divergence confirmation
if (smtDivergence) {
const divergenceDirection = smtDivergence.type;
const reversalDirection = sweepDirection === 'bullish' ? 'bearish' : 'bullish';
if (divergenceDirection === reversalDirection) {
reasoning.push(`SMT divergence confirms ${reversalDirection} reversal`);
}
}
// Calculate entry, stop, and target
const reversalDirection = sweepDirection === 'bullish' ? 'bearish' : 'bullish';
let entry: number;
let stopLoss: number;
let takeProfit: number;
if (reversalDirection === 'bearish') {
entry = londonData.high - londonData.range * 0.25; // Enter at 75% of range
stopLoss = londonData.high + londonData.range * 0.5; // Stop above sweep
takeProfit = londonData.low; // Target London low
} else {
entry = londonData.low + londonData.range * 0.25;
stopLoss = londonData.low - londonData.range * 0.5;
takeProfit = londonData.high;
}
// Calculate confidence
let confidence = 50; // Base
if (londonClusters.length >= 3) confidence += 10; // Multiple clusters
if (reclaimIndex <= 3) confidence += 10; // Very fast reclaim
if (smtDivergence) confidence += 15; // SMT confirmation
if (londonData.volumeProfile === 'decreasing') confidence += 5; // Volume profile
return {
type: 'london_flat_ny_reversal',
direction: reversalDirection,
confidence,
entry,
stopLoss,
takeProfit,
reasoning,
clusters: londonClusters,
timeframe: '15m',
};
}
function detectSteepSelloffBounce(
candles: Candle[],
sweeps: LiquiditySweep[],
fvgs: FairValueGap[],
pdl: number
): SessionPattern | null {
const reasoning: string[] = [];
// Check for rapid decline (> 1% in < 30 minutes)
const windowSize = 30; // 30 bars = 30 minutes on 1m chart
let maxDecline = 0;
let declineStartIndex = -1;
for (let i = windowSize; i < candles.length; i++) {
const windowCandles = candles.slice(i - windowSize, i);
const windowHigh = Math.max(...windowCandles.map(c => c.high));
const windowLow = Math.min(...windowCandles.map(c => c.low));
const decline = (windowHigh - windowLow) / windowHigh;
if (decline > maxDecline) {
maxDecline = decline;
declineStartIndex = i - windowSize;
}
}
if (maxDecline < 0.01) {
return null; // Need at least 1% decline
}
reasoning.push(`Rapid decline: ${(maxDecline * 100).toFixed(2)}% in 30 minutes`);
// Check for PDL sweep with volume spike
const pdlSweeps = sweeps.filter(
s => s.type === 'pdl_sweep' && s.volumeSpike
);
if (pdlSweeps.length === 0) {
return null;
}
reasoning.push(`PDL sweep detected at ${pdl.toFixed(2)} with volume spike`);
// Check for fast reclaim
const lastSweep = pdlSweeps[pdlSweeps.length - 1];
const sweepIndex = candles.findIndex(c => c.timestamp >= lastSweep.timestamp);
if (sweepIndex === -1) {
return null;
}
const reclaimIndex = candles.slice(sweepIndex).findIndex(
c => c.close > pdl
);
if (reclaimIndex === -1 || reclaimIndex > 5) {
return null; // No fast reclaim
}
reasoning.push(`Fast reclaim of PDL in ${reclaimIndex} bars`);
// Check for FVG during selloff
const selloffFVGs = fvgs.filter(
fvg => fvg.type === 'bullish' &&
fvg.startIndex >= declineStartIndex &&
fvg.startIndex < sweepIndex
);
if (selloffFVGs.length === 0) {
return null;
}
const targetFVG = selloffFVGs[selloffFVGs.length - 1];
reasoning.push(`Bullish FVG formed during selloff at ${targetFVG.gapLow.toFixed(2)}-${targetFVG.gapHigh.toFixed(2)}`);
// Check if price returned to FVG (50-75% fill)
const currentPrice = candles[candles.length - 1].close;
const fvgFillPercent = (currentPrice - targetFVG.gapLow) / (targetFVG.gapHigh - targetFVG.gapLow);
if (fvgFillPercent < 0.5 || fvgFillPercent > 0.75) {
return null;
}
reasoning.push(`Price at ${(fvgFillPercent * 100).toFixed(0)}% FVG fill (optimal entry zone)`);
// Calculate entry, stop, and target
const entry = targetFVG.gapLow + (targetFVG.gapHigh - targetFVG.gapLow) * 0.5;
const stopLoss = pdl - 5; // 5 points below PDL
const takeProfit = candles[declineStartIndex].high; // Target selloff origin
// Calculate confidence
let confidence = 50; // Base
if (reclaimIndex <= 3) confidence += 10; // Very fast reclaim
if (lastSweep.volumeSpike) confidence += 10; // Volume confirmation
if (fvgFillPercent >= 0.6 && fvgFillPercent <= 0.7) confidence += 10; // Optimal FVG fill
if (maxDecline >= 0.015) confidence += 5; // Very steep selloff
return {
type: 'steep_selloff_bounce',
direction: 'bullish',
confidence,
entry,
stopLoss,
takeProfit,
reasoning,
clusters: [], // Not cluster-based
timeframe: '1m',
};
}
function detectAccumulationCluster(
clusters: SweepCluster[],
sessionStart: Date,
sessionEnd: Date,
currentPrice: number
): SessionPattern | null {
const reasoning: string[] = [];
// Filter clusters within session window
const sessionClusters = clusters.filter(
c => c.centerTime >= sessionStart && c.centerTime <= sessionEnd
);
if (sessionClusters.length < 3) {
return null; // Need at least 3 clusters
}
// Calculate average density
const avgDensity =
sessionClusters.reduce((sum, c) => sum + c.density, 0) / sessionClusters.length;
if (avgDensity < 0.3) {
return null; // Not high-density accumulation
}
reasoning.push(`High-density clustering: ${avgDensity.toFixed(2)} sweeps/min`);
// Check for both bullish and bearish sweeps (range-bound)
const bullishClusters = sessionClusters.filter(c => c.direction === 'bullish');
const bearishClusters = sessionClusters.filter(c => c.direction === 'bearish');
if (bullishClusters.length === 0 || bearishClusters.length === 0) {
return null; // Need both directions
}
reasoning.push(`Both directions swept: ${bullishClusters.length} bullish, ${bearishClusters.length} bearish`);
// Calculate range
const allPrices = sessionClusters.flatMap(c => c.sweeps.map(s => s.level));
const rangeHigh = Math.max(...allPrices);
const rangeLow = Math.min(...allPrices);
const rangeSize = rangeHigh - rangeLow;
const rangePercent = rangeSize / rangeLow;
if (rangePercent > 0.01) {
return null; // Range too wide (> 1%)
}
reasoning.push(`Tight range: ${(rangePercent * 100).toFixed(2)}%`);
// Determine breakout direction (opposite of last cluster)
const lastCluster = sessionClusters[sessionClusters.length - 1];
const breakoutDirection = lastCluster.direction === 'bullish' ? 'bearish' : 'bullish';
reasoning.push(`Breakout direction: ${breakoutDirection} (opposite last sweep)`);
// Calculate entry, stop, and target
let entry: number;
let stopLoss: number;
let takeProfit: number;
if (breakoutDirection === 'bullish') {
entry = rangeHigh + rangeSize * 0.25; // Enter above range
stopLoss = rangeLow - rangeSize * 0.5; // Stop below range
takeProfit = rangeHigh + rangeSize * 2; // Target 2x range
} else {
entry = rangeLow - rangeSize * 0.25;
stopLoss = rangeHigh + rangeSize * 0.5;
takeProfit = rangeLow - rangeSize * 2;
}
// Calculate confidence
let confidence = 50; // Base
if (avgDensity >= 0.5) confidence += 10; // Very high density
if (sessionClusters.length >= 5) confidence += 10; // Many clusters
if (rangePercent < 0.005) confidence += 10; // Very tight range
return {
type: 'accumulation_cluster',
direction: breakoutDirection,
confidence,
entry,
stopLoss,
takeProfit,
reasoning,
clusters: sessionClusters,
timeframe: '15m',
};
}
3. Multi-Timeframe Integration
interface MultitimeframeAnalysis {
m1Pattern: SessionPattern | null;
m15Pattern: SessionPattern | null;
h1Pattern: SessionPattern | null;
dailyBias: 'bullish' | 'bearish' | 'neutral';
confidence: MultitimeframeConfidence;
primarySignal: SessionPattern | null;
}
async function analyzeMultitimeframe(
symbol: 'ES' | 'NQ',
currentTime: Date
): Promise<MultitimeframeAnalysis> {
// Fetch candles for all timeframes
const m1Candles = await fetchCandles(symbol, '1m', 100);
const m15Candles = await fetchCandles(symbol, '15m', 100);
const h1Candles = await fetchCandles(symbol, '1h', 100);
const dailyCandles = await fetchCandles(symbol, '1d', 30);
// Detect patterns on each timeframe
const m1Pattern = await detectPatternsOnTimeframe(m1Candles, '1m');
const m15Pattern = await detectPatternsOnTimeframe(m15Candles, '15m');
const h1Pattern = await detectPatternsOnTimeframe(h1Candles, '1h');
// Determine daily bias
const dailyBias = calculateDailyBias(dailyCandles);
// Determine primary signal (highest confidence)
const patterns = [m1Pattern, m15Pattern, h1Pattern].filter(p => p !== null);
const primarySignal = patterns.length > 0
? patterns.reduce((best, current) =>
current!.confidence > best!.confidence ? current : best
)
: null;
// Calculate multi-timeframe confidence
const confidence = primarySignal
? calculateConfidence(
m1Pattern,
m15Pattern,
h1Pattern,
dailyBias,
primarySignal.direction
)
: { base: 50, m1Alignment: 0, m15Alignment: 0, h1Alignment: 0, dailyAlignment: 0, total: 50 };
return {
m1Pattern,
m15Pattern,
h1Pattern,
dailyBias,
confidence,
primarySignal,
};
}
async function detectPatternsOnTimeframe(
candles: Candle[],
timeframe: string
): Promise<SessionPattern | null> {
// Get PDH/PDL levels
const { pdh, pdl } = getPriorDayLevels(candles);
// Detect sweeps
const sweeps = await detectAllSweeps(candles, pdh, pdl);
// Cluster sweeps
const clusters = clusterSweeps(sweeps);
// Detect FVGs
const fvgs = detectFairValueGaps(candles);
// Get session data
const sessionData = getSessionData(candles);
// Try each pattern type
let pattern: SessionPattern | null = null;
if (timeframe === '15m' || timeframe === '1h') {
pattern = detectLondonFlatNYReversal(
sessionData,
candles,
null, // SMT divergence would come from external analysis
candles[candles.length - 1].close
);
}
if (!pattern && timeframe === '1m') {
pattern = detectSteepSelloffBounce(candles, sweeps, fvgs, pdl);
}
if (!pattern) {
const now = new Date();
const sessionStart = new Date(now);
sessionStart.setHours(3, 0, 0, 0); // London session start
const sessionEnd = new Date(now);
sessionEnd.setHours(16, 0, 0, 0); // NY session end
pattern = detectAccumulationCluster(clusters, sessionStart, sessionEnd, candles[candles.length - 1].close);
}
return pattern;
}
function calculateDailyBias(dailyCandles: Candle[]): 'bullish' | 'bearish' | 'neutral' {
if (dailyCandles.length < 5) {
return 'neutral';
}
// Check last 5 days for trend
const recent = dailyCandles.slice(-5);
const closingPrices = recent.map(c => c.close);
// Simple trend: compare first and last close
const priceChange = closingPrices[4] - closingPrices[0];
const priceChangePercent = priceChange / closingPrices[0];
if (priceChangePercent > 0.01) {
return 'bullish'; // > 1% gain over 5 days
} else if (priceChangePercent < -0.01) {
return 'bearish'; // > 1% loss over 5 days
} else {
return 'neutral';
}
}
function getSessionData(candles: Candle[]): SessionData {
// Filter for London session (3 AM - 12 PM ET)
const londonStart = new Date(candles[0].timestamp);
londonStart.setHours(3, 0, 0, 0);
const londonEnd = new Date(candles[0].timestamp);
londonEnd.setHours(12, 0, 0, 0);
const londonCandles = candles.filter(
c => c.timestamp >= londonStart.getTime() && c.timestamp <= londonEnd.getTime()
);
if (londonCandles.length === 0) {
return {
open: 0,
high: 0,
low: 0,
close: 0,
range: 0,
rangePercent: 0,
volumeProfile: 'stable',
sweepClusters: [],
};
}
const open = londonCandles[0].open;
const close = londonCandles[londonCandles.length - 1].close;
const high = Math.max(...londonCandles.map(c => c.high));
const low = Math.min(...londonCandles.map(c => c.low));
const range = high - low;
const rangePercent = (range / open) * 100;
// Determine volume profile (simplified)
const firstHalfVolume = londonCandles.slice(0, Math.floor(londonCandles.length / 2))
.reduce((sum, c) => sum + c.volume, 0);
const secondHalfVolume = londonCandles.slice(Math.floor(londonCandles.length / 2))
.reduce((sum, c) => sum + c.volume, 0);
let volumeProfile: 'increasing' | 'decreasing' | 'stable';
if (secondHalfVolume > firstHalfVolume * 1.2) {
volumeProfile = 'increasing';
} else if (secondHalfVolume < firstHalfVolume * 0.8) {
volumeProfile = 'decreasing';
} else {
volumeProfile = 'stable';
}
return {
open,
high,
low,
close,
range,
rangePercent,
volumeProfile,
sweepClusters: [],
};
}
4. Discord Bot Integration Example
import { Client, EmbedBuilder, TextChannel } from 'discord.js';
class TJRSessionPatternBot {
private client: Client;
private channelId: string;
constructor(token: string, channelId: string) {
this.client = new Client({
intents: ['Guilds', 'GuildMessages'],
});
this.channelId = channelId;
this.client.once('ready', () => {
console.log('TJR Session Pattern Bot ready');
this.startAnalysis();
});
this.client.login(token);
}
private async startAnalysis() {
// Run analysis every 5 minutes during trading hours
setInterval(async () => {
const now = new Date();
const hour = now.getUTCHours() - 5; // Convert to ET
// Only run during London + NY sessions (3 AM - 4 PM ET)
if (hour >= 3 && hour < 16) {
await this.analyzeAndNotify();
}
}, 5 * 60 * 1000); // 5 minutes
}
private async analyzeAndNotify() {
try {
// Analyze both ES and NQ
const esAnalysis = await analyzeMultitimeframe('ES', new Date());
const nqAnalysis = await analyzeMultitimeframe('NQ', new Date());
// Check for high-confidence signals
if (esAnalysis.primarySignal && esAnalysis.confidence.total >= 75) {
await this.sendSignal('ES', esAnalysis);
}
if (nqAnalysis.primarySignal && nqAnalysis.confidence.total >= 75) {
await this.sendSignal('NQ', nqAnalysis);
}
// Check for SMT divergence between ES and NQ
if (
esAnalysis.primarySignal &&
nqAnalysis.primarySignal &&
esAnalysis.primarySignal.direction !== nqAnalysis.primarySignal.direction
) {
await this.sendSMTAlert(esAnalysis, nqAnalysis);
}
} catch (error) {
console.error('Analysis error:', error);
}
}
private async sendSignal(symbol: 'ES' | 'NQ', analysis: MultitimeframeAnalysis) {
const channel = await this.client.channels.fetch(this.channelId) as TextChannel;
const signal = analysis.primarySignal!;
const embed = new EmbedBuilder()
.setTitle(`🎯 TJR ${signal.type.toUpperCase()} - ${symbol}`)
.setColor(signal.direction === 'bullish' ? 0x00FF00 : 0xFF0000)
.addFields(
{ name: 'Direction', value: signal.direction.toUpperCase(), inline: true },
{ name: 'Confidence', value: `${analysis.confidence.total}%`, inline: true },
{ name: 'Timeframe', value: signal.timeframe, inline: true },
{ name: 'Entry', value: signal.entry.toFixed(2), inline: true },
{ name: 'Stop Loss', value: signal.stopLoss.toFixed(2), inline: true },
{ name: 'Take Profit', value: signal.takeProfit.toFixed(2), inline: true },
{
name: 'Multi-Timeframe Alignment',
value: `1m: ${analysis.confidence.m1Alignment > 0 ? '✅' : '❌'} | ` +
`15m: ${analysis.confidence.m15Alignment > 0 ? '✅' : '❌'} | ` +
`1h: ${analysis.confidence.h1Alignment > 0 ? '✅' : '❌'} | ` +
`Daily: ${analysis.confidence.dailyAlignment > 0 ? '✅' : '❌'}`,
inline: false,
},
{
name: 'Reasoning',
value: signal.reasoning.join('\n'),
inline: false,
}
)
.setTimestamp();
await channel.send({ embeds: [embed] });
}
private async sendSMTAlert(
esAnalysis: MultitimeframeAnalysis,
nqAnalysis: MultitimeframeAnalysis
) {
const channel = await this.client.channels.fetch(this.channelId) as TextChannel;
const embed = new EmbedBuilder()
.setTitle('⚠️ SMT DIVERGENCE DETECTED')
.setColor(0xFFFF00)
.addFields(
{
name: 'ES Pattern',
value: `${esAnalysis.primarySignal!.type} (${esAnalysis.primarySignal!.direction})`,
inline: true,
},
{
name: 'NQ Pattern',
value: `${nqAnalysis.primarySignal!.type} (${nqAnalysis.primarySignal!.direction})`,
inline: true,
},
{
name: 'Implication',
value: 'Potential reversal or weakening trend. Monitor for confirmation.',
inline: false,
}
)
.setTimestamp();
await channel.send({ embeds: [embed] });
}
}
// Usage
const bot = new TJRSessionPatternBot(
process.env.DISCORD_TOKEN!,
process.env.CHANNEL_ID!
);
Common Patterns and Best Practices
Pattern Selection Priority
-
London Flat → NY Reversal (Highest Priority)
- Only valid during NY open window (9:30-10:30 AM ET)
- Requires London session data (3 AM - 9:30 AM)
- Best on 15-minute timeframe
- Confidence boost: SMT divergence, fast reclaim
-
Steep Selloff → Bounce (Medium Priority)
- Valid any time during trading hours
- Best on 1-minute timeframe for precision
- Requires FVG during selloff
- Confidence boost: Volume spike, optimal FVG fill (60-70%)
-
Accumulation Cluster (Lower Priority)
- Valid during extended consolidation periods
- Best on 15-minute or 1-hour timeframe
- Requires multi-hour observation window
- Confidence boost: Very high density (≥ 0.5), tight range (< 0.5%)
Risk Management
interface RiskParameters {
maxRiskPercent: number; // Default: 1% of account
maxPositionSize: number; // Default: 2 contracts
minConfidence: number; // Default: 70%
minRiskReward: number; // Default: 2.0
}
function validateRisk(
signal: SessionPattern,
accountBalance: number,
params: RiskParameters
): boolean {
// Check minimum confidence
if (signal.confidence < params.minConfidence) {
return false;
}
// Calculate risk/reward
const risk = Math.abs(signal.entry - signal.stopLoss);
const reward = Math.abs(signal.takeProfit - signal.entry);
const riskReward = reward / risk;
if (riskReward < params.minRiskReward) {
return false;
}
// Calculate position size
const dollarRisk = accountBalance * (params.maxRiskPercent / 100);
const pointValue = 50; // $50 per point for ES/NQ
const maxContracts = Math.floor(dollarRisk / (risk * pointValue));
if (maxContracts > params.maxPositionSize) {
return false;
}
return true;
}
Session Timing Validation
function isValidSessionTiming(
pattern: SessionPattern,
currentTime: Date
): boolean {
const hour = currentTime.getUTCHours() - 5; // Convert to ET
const minute = currentTime.getUTCMinutes();
switch (pattern.type) {
case 'london_flat_ny_reversal':
// Only valid during NY open (9:30-10:30 AM ET)
return hour === 9 && minute >= 30 || hour === 10 && minute < 30;
case 'steep_selloff_bounce':
// Valid during all trading hours (3 AM - 4 PM ET)
return hour >= 3 && hour < 16;
case 'accumulation_cluster':
// Valid during London and NY sessions
return hour >= 3 && hour < 16;
default:
return false;
}
}
Complete Example: Session Pattern Strategy
import { Client } from 'discord.js';
async function main() {
const bot = new TJRSessionPatternBot(
process.env.DISCORD_TOKEN!,
process.env.CHANNEL_ID!
);
// Example: Manual analysis
const analysis = await analyzeMultitimeframe('ES', new Date());
console.log('Multi-Timeframe Analysis:');
console.log('========================');
console.log(`1-Minute Pattern: ${analysis.m1Pattern?.type || 'None'}`);
console.log(`15-Minute Pattern: ${analysis.m15Pattern?.type || 'None'}`);
console.log(`1-Hour Pattern: ${analysis.h1Pattern?.type || 'None'}`);
console.log(`Daily Bias: ${analysis.dailyBias}`);
console.log();
if (analysis.primarySignal) {
console.log('Primary Signal:');
console.log(` Type: ${analysis.primarySignal.type}`);
console.log(` Direction: ${analysis.primarySignal.direction}`);
console.log(` Confidence: ${analysis.confidence.total}%`);
console.log(` Entry: ${analysis.primarySignal.entry.toFixed(2)}`);
console.log(` Stop Loss: ${analysis.primarySignal.stopLoss.toFixed(2)}`);
console.log(` Take Profit: ${analysis.primarySignal.takeProfit.toFixed(2)}`);
console.log();
console.log(' Reasoning:');
analysis.primarySignal.reasoning.forEach(r => console.log(` - ${r}`));
} else {
console.log('No high-confidence signal detected.');
}
}
// Run
main().catch(console.error);
Summary
This skill provides comprehensive reference material for implementing TJR's session pattern recognition system. Key takeaways:
- Sweep Clustering: Group liquidity events using 15-minute time and 0.15% price tolerance
- Session Patterns: Three primary patterns with specific validation criteria
- Multi-Timeframe Ladder: Build confidence from 1m → 15m → 1h → daily alignment
- Confidence Scoring: 50% base + weighted bonuses for alignment and confirmations
- Risk Management: Validate minimum confidence (70%), risk/reward (2:1), position sizing
When to use: Building Discord trading bots that need sophisticated pattern recognition, session analysis, or multi-timeframe confluence validation for ES/NQ futures.
Reuses from other skills:
trading-foundations: Candle models, swing points, timeframe resamplingtjr-liquidity-detection: PDH/PDL sweeps, FVG detection, VWAP validationtjr-smt-divergence: ES/NQ correlation, structure divergence detection
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?