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:
@@ -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) {
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,12 +38,33 @@ 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[] = []
|
||||||
|
|
||||||
// ATR check (volatility gate: 0.15% - 2.5%)
|
// Detect 5-minute timeframe
|
||||||
|
const is5min = params.timeframe === '5' || params.timeframe === 'manual'
|
||||||
|
|
||||||
|
// ATR check - TIMEFRAME AWARE
|
||||||
if (params.atr > 0) {
|
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) {
|
if (params.atr < 0.15) {
|
||||||
score -= 15
|
score -= 15
|
||||||
reasons.push(`ATR too low (${params.atr.toFixed(2)}% - dead market)`)
|
reasons.push(`ATR too low (${params.atr.toFixed(2)}% - dead market)`)
|
||||||
@@ -54,9 +79,24 @@ export function scoreSignalQuality(params: {
|
|||||||
reasons.push(`ATR healthy (${params.atr.toFixed(2)}%)`)
|
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 (is5min) {
|
||||||
|
// 5min: ADX 15+ is actually trending, 20+ is strong
|
||||||
|
if (params.adx > 22) {
|
||||||
|
score += 15
|
||||||
|
reasons.push(`Strong 5min 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 {
|
||||||
|
// Higher timeframes: stricter ADX requirements
|
||||||
if (params.adx > 25) {
|
if (params.adx > 25) {
|
||||||
score += 15
|
score += 15
|
||||||
reasons.push(`Strong trend (ADX ${params.adx.toFixed(1)})`)
|
reasons.push(`Strong trend (ADX ${params.adx.toFixed(1)})`)
|
||||||
@@ -68,6 +108,7 @@ export function scoreSignalQuality(params: {
|
|||||||
reasons.push(`Moderate trend (ADX ${params.adx.toFixed(1)})`)
|
reasons.push(`Moderate trend (ADX ${params.adx.toFixed(1)})`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RSI check (momentum confirmation)
|
// RSI check (momentum confirmation)
|
||||||
if (params.rsi > 0) {
|
if (params.rsi > 0) {
|
||||||
@@ -136,6 +177,12 @@ export function scoreSignalQuality(params: {
|
|||||||
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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user