/** * Signal Quality Scoring * * Unified quality scoring logic used by both check-risk and execute endpoints. * Ensures consistent scoring across the trading pipeline. */ export interface SignalQualityResult { score: number passed: boolean reasons: string[] } /** * Calculate signal quality score based on technical indicators * * TIMEFRAME-AWARE SCORING: * 5min charts naturally have lower ADX/ATR than higher timeframes * * Scoring breakdown: * - Base: 50 points * - ATR (volatility): -20 to +10 points (5min: 0.25-0.7% is healthy) * - ADX (trend strength): -15 to +15 points (5min: 15+ is trending) * - RSI (momentum): -10 to +10 points * - Volume: -10 to +15 points * - Price position: -15 to +5 points * - Volume breakout bonus: +10 points * - Anti-chop filter: -20 points (5min only, extreme chop) * * Total range: ~15-115 points (realistically 30-100) * Threshold: 60 points minimum for execution */ export function scoreSignalQuality(params: { atr: number adx: number rsi: number volumeRatio: number pricePosition: number direction: 'long' | 'short' minScore?: number // Configurable minimum score threshold timeframe?: string // e.g., '5', '15', '60', '1D' }): SignalQualityResult { let score = 50 // Base score const reasons: string[] = [] // Detect 5-minute timeframe const is5min = params.timeframe === '5' || params.timeframe === 'manual' // ATR check - TIMEFRAME AWARE if (params.atr > 0) { if (is5min) { // 5min: lower thresholds, more lenient if (params.atr < 0.2) { score -= 15 reasons.push(`ATR too low (${params.atr.toFixed(2)}% - dead market)`) } else if (params.atr > 1.5) { score -= 20 reasons.push(`ATR too high (${params.atr.toFixed(2)}% - too volatile)`) } else if (params.atr >= 0.2 && params.atr < 0.35) { score += 5 reasons.push(`ATR acceptable (${params.atr.toFixed(2)}%)`) } else { score += 10 reasons.push(`ATR healthy (${params.atr.toFixed(2)}%)`) } } else { // Higher timeframes: stricter requirements if (params.atr < 0.15) { score -= 15 reasons.push(`ATR too low (${params.atr.toFixed(2)}% - dead market)`) } else if (params.atr > 2.5) { score -= 20 reasons.push(`ATR too high (${params.atr.toFixed(2)}% - too volatile)`) } else if (params.atr >= 0.15 && params.atr < 0.4) { score += 5 reasons.push(`ATR moderate (${params.atr.toFixed(2)}%)`) } else { score += 10 reasons.push(`ATR healthy (${params.atr.toFixed(2)}%)`) } } } // ADX check - TIMEFRAME AWARE if (params.adx > 0) { if (is5min) { // 5min: ADX 15+ is actually trending, 20+ is strong // High volume can compensate for lower ADX in breakouts/breakdowns const hasStrongVolume = params.volumeRatio > 1.2 if (params.adx > 22) { score += 15 reasons.push(`Strong 5min trend (ADX ${params.adx.toFixed(1)})`) } else if (params.adx < 12) { // Reduce penalty if strong volume present (breakdown/breakout in progress) if (hasStrongVolume) { score -= 5 reasons.push(`Lower 5min ADX (${params.adx.toFixed(1)}) but strong volume compensates`) } else { score -= 15 reasons.push(`Weak 5min trend (ADX ${params.adx.toFixed(1)})`) } } else { score += 5 reasons.push(`Moderate 5min trend (ADX ${params.adx.toFixed(1)})`) } } else { // Higher timeframes: stricter ADX requirements const hasStrongVolume = params.volumeRatio > 1.2 if (params.adx > 25) { score += 15 reasons.push(`Strong trend (ADX ${params.adx.toFixed(1)})`) } else if (params.adx < 18) { // Reduce penalty if strong volume present if (hasStrongVolume) { score -= 5 reasons.push(`Lower ADX (${params.adx.toFixed(1)}) but strong volume compensates`) } else { score -= 15 reasons.push(`Weak trend (ADX ${params.adx.toFixed(1)})`) } } else { score += 5 reasons.push(`Moderate trend (ADX ${params.adx.toFixed(1)})`) } } } // RSI check (momentum confirmation) if (params.rsi > 0) { if (params.direction === 'long') { if (params.rsi > 50 && params.rsi < 70) { score += 10 reasons.push(`RSI supports long (${params.rsi.toFixed(1)})`) } else if (params.rsi > 70) { score -= 10 reasons.push(`RSI overbought (${params.rsi.toFixed(1)})`) } } else { // short if (params.rsi < 50 && params.rsi > 30) { score += 10 reasons.push(`RSI supports short (${params.rsi.toFixed(1)})`) } else if (params.rsi < 30) { score -= 10 reasons.push(`RSI oversold (${params.rsi.toFixed(1)})`) } } } // Volume check (want > 1.0 = above average) if (params.volumeRatio > 0) { if (params.volumeRatio > 1.5) { score += 15 reasons.push(`Very strong volume (${params.volumeRatio.toFixed(2)}x avg)`) } else if (params.volumeRatio > 1.2) { score += 10 reasons.push(`Strong volume (${params.volumeRatio.toFixed(2)}x avg)`) } else if (params.volumeRatio < 0.8) { score -= 10 reasons.push(`Weak volume (${params.volumeRatio.toFixed(2)}x avg)`) } } // Price position check (avoid chasing vs breakout/breakdown detection) if (params.pricePosition > 0) { if (params.direction === 'long' && params.pricePosition > 95) { // High volume breakout at range top can be good if (params.volumeRatio > 1.4) { score += 5 reasons.push(`Volume breakout at range top (${params.pricePosition.toFixed(0)}%, vol ${params.volumeRatio.toFixed(2)}x)`) } else { score -= 15 reasons.push(`Price near top of range (${params.pricePosition.toFixed(0)}%) - risky long`) } } else if (params.direction === 'short' && params.pricePosition < 5) { // High volume breakdown at range bottom can be good // Also allow if RSI is bearish (< 40) - breakdowns continue lower if (params.volumeRatio > 1.2 || (params.rsi > 0 && params.rsi < 40)) { score += 5 reasons.push(`Valid breakdown at range bottom (${params.pricePosition.toFixed(0)}%, vol ${params.volumeRatio.toFixed(2)}x, RSI ${params.rsi.toFixed(1)})`) } else { score -= 10 reasons.push(`Price near bottom of range (${params.pricePosition.toFixed(0)}%) - reduced penalty for short`) } } else if (params.direction === 'long' && params.pricePosition < 5) { // Longs at bottom with good volume = potential reversal if (params.volumeRatio > 1.2 || (params.rsi > 0 && params.rsi > 60)) { score += 5 reasons.push(`Potential reversal at bottom (${params.pricePosition.toFixed(0)}%, vol ${params.volumeRatio.toFixed(2)}x, RSI ${params.rsi.toFixed(1)})`) } else { score -= 10 reasons.push(`Price near bottom (${params.pricePosition.toFixed(0)}%) - reduced penalty for reversal long`) } } else { score += 5 reasons.push(`Price position OK (${params.pricePosition.toFixed(0)}%)`) } } // Volume breakout bonus (high volume can override other weaknesses) if (params.volumeRatio > 1.8 && params.atr < 0.6) { score += 10 reasons.push(`Volume breakout compensates for low ATR`) } // ANTI-CHOP FILTER for 5min (extreme penalty for sideways chop) if (is5min && params.adx < 10 && params.atr < 0.25 && params.volumeRatio < 0.9) { score -= 20 reasons.push(`⛔ Extreme chop detected (ADX ${params.adx.toFixed(1)}, ATR ${params.atr.toFixed(2)}%, Vol ${params.volumeRatio.toFixed(2)}x)`) } const minScore = params.minScore || 60 const passed = score >= minScore return { score, passed, reasons, } }