Files
trading_bot_v4/lib/trading/signal-quality.ts
mindesbunister 3c9da22a8a Add ADX > 18 requirement for extreme price positions
- Shorts/longs at < 15% range require ADX > 18 AND volume > 1.2x
- OR RSI < 35 for shorts, RSI > 60 for longs
- Increased penalty from -10 to -15 when conditions not met
- Changed threshold from < 5% to < 15% to catch more edge cases

Test results:
- Big loser (01:35): ADX 16.1, price 9.3% → Score 60 (was 90) → BLOCKED
- Today's signal (10:05): ADX 17.3, price 0.9% → Score 55 (was 85) → BLOCKED

Rationale: False breakdowns in choppy ranges (ADX < 18) cause losses.
Tradeoff: May block some profitable breakdowns, but prevents chop losses.
2025-11-07 12:19:41 +01:00

233 lines
8.4 KiB
TypeScript

/**
* 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 < 15) {
// Shorts near range bottom (< 15%) require strong confirmation
// Require STRONG trend (ADX > 18) to avoid false breakdowns in choppy ranges
// OR very bearish RSI (< 35) indicating strong momentum continuation
const hasStrongTrend = params.adx > 18
const isVeryBearish = params.rsi > 0 && params.rsi < 35
const hasGoodVolume = params.volumeRatio > 1.2
if ((hasGoodVolume && hasStrongTrend) || isVeryBearish) {
score += 5
reasons.push(`Valid breakdown at range bottom (${params.pricePosition.toFixed(0)}%, vol ${params.volumeRatio.toFixed(2)}x, ADX ${params.adx.toFixed(1)}, RSI ${params.rsi.toFixed(1)})`)
} else {
score -= 15
reasons.push(`Price near bottom (${params.pricePosition.toFixed(0)}%) - need ADX > 18 or RSI < 35 for breakdown`)
}
} else if (params.direction === 'long' && params.pricePosition < 15) {
// Longs near range bottom (< 15%) require strong reversal confirmation
// Require STRONG trend (ADX > 18) to avoid catching falling knives
// OR very bullish RSI (> 60) after bounce showing momentum shift
const hasStrongTrend = params.adx > 18
const isVeryBullish = params.rsi > 0 && params.rsi > 60
const hasGoodVolume = params.volumeRatio > 1.2
if ((hasGoodVolume && hasStrongTrend) || isVeryBullish) {
score += 5
reasons.push(`Potential reversal at bottom (${params.pricePosition.toFixed(0)}%, vol ${params.volumeRatio.toFixed(2)}x, ADX ${params.adx.toFixed(1)}, RSI ${params.rsi.toFixed(1)})`)
} else {
score -= 15
reasons.push(`Price near bottom (${params.pricePosition.toFixed(0)}%) - need ADX > 18 or RSI > 60 for reversal`)
}
} 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,
}
}