Add timeframe-aware signal quality scoring for 5min charts

- Lower ADX/ATR thresholds for 5min timeframe (ADX 12-22, ATR 0.2-0.7%)
- Add anti-chop filter: -20 points for extreme sideways (ADX<10, ATR<0.25, Vol<0.9)
- Pass timeframe parameter through check-risk and execute endpoints
- Fixes flip-flop losses from overly strict 5min filters
- Higher timeframes unchanged (still use ADX 18+, ATR 0.4+)

5min scoring now:
- ADX 12-15: moderate trend (+5)
- ADX 22+: strong trend (+15)
- ATR 0.2-0.35: acceptable (+5)
- ATR 0.35+: healthy (+10)
- Extreme chop penalty prevents whipsaw trades
This commit is contained in:
mindesbunister
2025-11-07 08:56:19 +01:00
parent b52a980138
commit 0365560c5b
3 changed files with 76 additions and 24 deletions

View File

@@ -15,6 +15,7 @@ import { scoreSignalQuality, SignalQualityResult } from '@/lib/trading/signal-qu
export interface RiskCheckRequest { export interface RiskCheckRequest {
symbol: string symbol: string
direction: 'long' | 'short' direction: 'long' | 'short'
timeframe?: string // e.g., '5', '15', '60', '1D'
// Optional context metrics from TradingView // Optional context metrics from TradingView
atr?: number atr?: number
adx?: number adx?: number
@@ -57,6 +58,7 @@ function shouldAllowScaling(
pricePosition: newSignal.pricePosition, pricePosition: newSignal.pricePosition,
direction: newSignal.direction, direction: newSignal.direction,
minScore: config.minScaleQualityScore, minScore: config.minScaleQualityScore,
timeframe: newSignal.timeframe,
}) })
// 2. Check quality score (higher bar than initial entry) // 2. Check quality score (higher bar than initial entry)
@@ -271,7 +273,8 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
volumeRatio: body.volumeRatio || 0, volumeRatio: body.volumeRatio || 0,
pricePosition: body.pricePosition || 0, pricePosition: body.pricePosition || 0,
direction: body.direction, direction: body.direction,
minScore: 60 // Hardcoded threshold minScore: 60, // Hardcoded threshold
timeframe: body.timeframe,
}) })
if (!qualityScore.passed) { if (!qualityScore.passed) {

View File

@@ -322,6 +322,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
volumeRatio: body.volumeRatio || 0, volumeRatio: body.volumeRatio || 0,
pricePosition: body.pricePosition || 0, pricePosition: body.pricePosition || 0,
direction: body.direction, direction: body.direction,
timeframe: body.timeframe,
}) })
await createTrade({ await createTrade({
@@ -550,6 +551,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
volumeRatio: body.volumeRatio || 0, volumeRatio: body.volumeRatio || 0,
pricePosition: body.pricePosition || 0, pricePosition: body.pricePosition || 0,
direction: body.direction, direction: body.direction,
timeframe: body.timeframe,
}) })
await createTrade({ await createTrade({

View File

@@ -14,14 +14,18 @@ export interface SignalQualityResult {
/** /**
* Calculate signal quality score based on technical indicators * Calculate signal quality score based on technical indicators
* *
* TIMEFRAME-AWARE SCORING:
* 5min charts naturally have lower ADX/ATR than higher timeframes
*
* Scoring breakdown: * Scoring breakdown:
* - Base: 50 points * - Base: 50 points
* - ATR (volatility): -20 to +10 points * - ATR (volatility): -20 to +10 points (5min: 0.25-0.7% is healthy)
* - ADX (trend strength): -15 to +15 points * - ADX (trend strength): -15 to +15 points (5min: 15+ is trending)
* - RSI (momentum): -10 to +10 points * - RSI (momentum): -10 to +10 points
* - Volume: -10 to +15 points * - Volume: -10 to +15 points
* - Price position: -15 to +5 points * - Price position: -15 to +5 points
* - Volume breakout bonus: +10 points * - Volume breakout bonus: +10 points
* - Anti-chop filter: -20 points (5min only, extreme chop)
* *
* Total range: ~15-115 points (realistically 30-100) * Total range: ~15-115 points (realistically 30-100)
* Threshold: 60 points minimum for execution * Threshold: 60 points minimum for execution
@@ -34,38 +38,75 @@ export function scoreSignalQuality(params: {
pricePosition: number pricePosition: number
direction: 'long' | 'short' direction: 'long' | 'short'
minScore?: number // Configurable minimum score threshold minScore?: number // Configurable minimum score threshold
timeframe?: string // e.g., '5', '15', '60', '1D'
}): SignalQualityResult { }): SignalQualityResult {
let score = 50 // Base score let score = 50 // Base score
const reasons: string[] = [] const reasons: string[] = []
// Detect 5-minute timeframe
const is5min = params.timeframe === '5' || params.timeframe === 'manual'
// ATR check (volatility gate: 0.15% - 2.5%) // ATR check - TIMEFRAME AWARE
if (params.atr > 0) { if (params.atr > 0) {
if (params.atr < 0.15) { if (is5min) {
score -= 15 // 5min: lower thresholds, more lenient
reasons.push(`ATR too low (${params.atr.toFixed(2)}% - dead market)`) if (params.atr < 0.2) {
} else if (params.atr > 2.5) { score -= 15
score -= 20 reasons.push(`ATR too low (${params.atr.toFixed(2)}% - dead market)`)
reasons.push(`ATR too high (${params.atr.toFixed(2)}% - too volatile)`) } else if (params.atr > 1.5) {
} else if (params.atr >= 0.15 && params.atr < 0.4) { score -= 20
score += 5 reasons.push(`ATR too high (${params.atr.toFixed(2)}% - too volatile)`)
reasons.push(`ATR moderate (${params.atr.toFixed(2)}%)`) } 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 { } else {
score += 10 // Higher timeframes: stricter requirements
reasons.push(`ATR healthy (${params.atr.toFixed(2)}%)`) 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 (trend strength: want >18) // ADX check - TIMEFRAME AWARE
if (params.adx > 0) { if (params.adx > 0) {
if (params.adx > 25) { if (is5min) {
score += 15 // 5min: ADX 15+ is actually trending, 20+ is strong
reasons.push(`Strong trend (ADX ${params.adx.toFixed(1)})`) if (params.adx > 22) {
} else if (params.adx < 18) { score += 15
score -= 15 reasons.push(`Strong 5min trend (ADX ${params.adx.toFixed(1)})`)
reasons.push(`Weak trend (ADX ${params.adx.toFixed(1)})`) } else if (params.adx < 12) {
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 { } else {
score += 5 // Higher timeframes: stricter ADX requirements
reasons.push(`Moderate trend (ADX ${params.adx.toFixed(1)})`) if (params.adx > 25) {
score += 15
reasons.push(`Strong trend (ADX ${params.adx.toFixed(1)})`)
} else if (params.adx < 18) {
score -= 15
reasons.push(`Weak trend (ADX ${params.adx.toFixed(1)})`)
} else {
score += 5
reasons.push(`Moderate trend (ADX ${params.adx.toFixed(1)})`)
}
} }
} }
@@ -135,6 +176,12 @@ export function scoreSignalQuality(params: {
score += 10 score += 10
reasons.push(`Volume breakout compensates for low ATR`) 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 minScore = params.minScore || 60
const passed = score >= minScore const passed = score >= minScore