Fix: Add timeframe-aware signal quality scoring for 5min charts
PROBLEM: - Long signal (ADX 15.7, ATR 0.35%) blocked with score 45/100 - Missed major +3% runup, lost -2 on short that didn't flip - Scoring logic treated all timeframes identically (daily chart thresholds) ROOT CAUSE: - ADX < 18 always scored -15 points regardless of timeframe - 5min charts naturally have lower ADX (12-22 healthy range) - copilot-instructions mentioned timeframe awareness but wasn't implemented FIX: - Add timeframe parameter to RiskCheckRequest interface - Update scoreSignalQuality() with timeframe-aware ADX thresholds: * 5min/15min: ADX 12-22 healthy (+5), <12 weak (-15), >22 strong (+15) * Higher TF: ADX 18-25 healthy (+5), <18 weak (-15), >25 strong (+15) - Pass timeframe from n8n workflow through check-risk and execute - Update both Check Risk nodes in Money Machine workflow IMPACT: Your blocked signal (ADX 15.7 on 5min) now scores: - Was: 50 + 5 - 15 + 0 + 0 + 5 = 45 (BLOCKED) - Now: 50 + 5 + 5 + 0 + 0 + 5 = 65 (PASSES) This 20-point improvement from timeframe awareness would have caught the runup.
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" for 5min, "60" for 1H, "D" for daily
|
||||||
// Optional context metrics from TradingView
|
// Optional context metrics from TradingView
|
||||||
atr?: number
|
atr?: number
|
||||||
adx?: number
|
adx?: number
|
||||||
@@ -270,6 +271,7 @@ 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,
|
||||||
|
timeframe: body.timeframe, // Pass timeframe for context-aware scoring
|
||||||
minScore: 60 // Hardcoded threshold
|
minScore: 60 // Hardcoded threshold
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -330,6 +330,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({
|
||||||
@@ -540,6 +541,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({
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ export interface SignalQualityResult {
|
|||||||
*
|
*
|
||||||
* 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
|
||||||
|
*
|
||||||
|
* TIMEFRAME-AWARE SCORING:
|
||||||
|
* - 5min charts have lower ADX/ATR thresholds (trends develop slower)
|
||||||
|
* - Higher timeframes require stronger confirmation
|
||||||
*/
|
*/
|
||||||
export function scoreSignalQuality(params: {
|
export function scoreSignalQuality(params: {
|
||||||
atr: number
|
atr: number
|
||||||
@@ -33,11 +37,17 @@ export function scoreSignalQuality(params: {
|
|||||||
volumeRatio: number
|
volumeRatio: number
|
||||||
pricePosition: number
|
pricePosition: number
|
||||||
direction: 'long' | 'short'
|
direction: 'long' | 'short'
|
||||||
|
timeframe?: string // "5" = 5min, "15" = 15min, "60" = 1H, "D" = daily
|
||||||
minScore?: number // Configurable minimum score threshold
|
minScore?: number // Configurable minimum score threshold
|
||||||
}): SignalQualityResult {
|
}): SignalQualityResult {
|
||||||
let score = 50 // Base score
|
let score = 50 // Base score
|
||||||
const reasons: string[] = []
|
const reasons: string[] = []
|
||||||
|
|
||||||
|
// Determine if this is a short timeframe (5min, 15min)
|
||||||
|
const is5minChart = params.timeframe === '5'
|
||||||
|
const is15minChart = params.timeframe === '15'
|
||||||
|
const isShortTimeframe = is5minChart || is15minChart
|
||||||
|
|
||||||
// ATR check (volatility gate: 0.15% - 2.5%)
|
// ATR check (volatility gate: 0.15% - 2.5%)
|
||||||
if (params.atr > 0) {
|
if (params.atr > 0) {
|
||||||
if (params.atr < 0.15) {
|
if (params.atr < 0.15) {
|
||||||
@@ -55,8 +65,24 @@ export function scoreSignalQuality(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ADX check (trend strength: want >18)
|
// ADX check - TIMEFRAME AWARE (trend strength)
|
||||||
|
// 5min/15min: 12-22 is healthy (trends develop slower)
|
||||||
|
// 1H+: 18+ is healthy (stronger trends expected)
|
||||||
if (params.adx > 0) {
|
if (params.adx > 0) {
|
||||||
|
if (isShortTimeframe) {
|
||||||
|
// 5min/15min thresholds
|
||||||
|
if (params.adx > 22) {
|
||||||
|
score += 15
|
||||||
|
reasons.push(`Strong trend for ${params.timeframe}min (ADX ${params.adx.toFixed(1)})`)
|
||||||
|
} else if (params.adx < 12) {
|
||||||
|
score -= 15
|
||||||
|
reasons.push(`Weak trend for ${params.timeframe}min (ADX ${params.adx.toFixed(1)})`)
|
||||||
|
} else {
|
||||||
|
score += 5
|
||||||
|
reasons.push(`Moderate trend for ${params.timeframe}min (ADX ${params.adx.toFixed(1)})`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Higher timeframe thresholds (1H, 4H, D)
|
||||||
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 +94,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) {
|
||||||
|
|||||||
@@ -463,7 +463,7 @@
|
|||||||
},
|
},
|
||||||
"sendBody": true,
|
"sendBody": true,
|
||||||
"specifyBody": "json",
|
"specifyBody": "json",
|
||||||
"jsonBody": "={\n \"symbol\": \"{{ $json.symbol }}\",\n \"direction\": \"{{ $json.direction }}\",\n \"atr\": {{ $json.atr || 0 }},\n \"adx\": {{ $json.adx || 0 }},\n \"rsi\": {{ $json.rsi || 0 }},\n \"volumeRatio\": {{ $json.volumeRatio || 0 }},\n \"pricePosition\": {{ $json.pricePosition || 0 }}\n}",
|
"jsonBody": "={\n \"symbol\": \"{{ $json.symbol }}\",\n \"direction\": \"{{ $json.direction }}\",\n \"timeframe\": \"{{ $json.timeframe }}\",\n \"atr\": {{ $json.atr || 0 }},\n \"adx\": {{ $json.adx || 0 }},\n \"rsi\": {{ $json.rsi || 0 }},\n \"volumeRatio\": {{ $json.volumeRatio || 0 }},\n \"pricePosition\": {{ $json.pricePosition || 0 }}\n}",
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"id": "55671044-c7c8-4566-b271-9369a1c43158",
|
"id": "55671044-c7c8-4566-b271-9369a1c43158",
|
||||||
|
|||||||
Reference in New Issue
Block a user