Implement signal quality scoring system
- Updated execute endpoint to store context metrics in database - Updated CreateTradeParams interface with 5 context metrics - Updated Prisma schema with rsiAtEntry and pricePositionAtEntry - Ran migration: add_rsi_and_price_position_metrics - Complete flow: TradingView → n8n → check-risk (scores) → execute (stores)
This commit is contained in:
@@ -13,12 +13,20 @@ import { getLastTradeTime, getTradesInLastHour, getTodayPnL } from '@/lib/databa
|
||||
export interface RiskCheckRequest {
|
||||
symbol: string
|
||||
direction: 'long' | 'short'
|
||||
// Optional context metrics from TradingView
|
||||
atr?: number
|
||||
adx?: number
|
||||
rsi?: number
|
||||
volumeRatio?: number
|
||||
pricePosition?: number
|
||||
}
|
||||
|
||||
export interface RiskCheckResponse {
|
||||
allowed: boolean
|
||||
reason?: string
|
||||
details?: string
|
||||
qualityScore?: number
|
||||
qualityReasons?: string[]
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest): Promise<NextResponse<RiskCheckResponse>> {
|
||||
@@ -135,6 +143,50 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Check signal quality (if context metrics provided)
|
||||
const hasContextMetrics = body.atr !== undefined && body.atr > 0
|
||||
|
||||
if (hasContextMetrics) {
|
||||
const qualityScore = scoreSignalQuality({
|
||||
atr: body.atr || 0,
|
||||
adx: body.adx || 0,
|
||||
rsi: body.rsi || 0,
|
||||
volumeRatio: body.volumeRatio || 0,
|
||||
pricePosition: body.pricePosition || 0,
|
||||
direction: body.direction
|
||||
})
|
||||
|
||||
if (!qualityScore.passed) {
|
||||
console.log('🚫 Risk check BLOCKED: Signal quality too low', {
|
||||
score: qualityScore.score,
|
||||
reasons: qualityScore.reasons
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
allowed: false,
|
||||
reason: 'Signal quality too low',
|
||||
details: `Score: ${qualityScore.score}/100 - ${qualityScore.reasons.join(', ')}`,
|
||||
qualityScore: qualityScore.score,
|
||||
qualityReasons: qualityScore.reasons
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`✅ Risk check PASSED: All checks passed`, {
|
||||
todayPnL: todayPnL.toFixed(2),
|
||||
tradesLastHour: tradesInLastHour,
|
||||
cooldownPassed: lastTradeTime ? 'yes' : 'no previous trades',
|
||||
qualityScore: qualityScore.score,
|
||||
qualityReasons: qualityScore.reasons
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
allowed: true,
|
||||
details: 'All risk checks passed',
|
||||
qualityScore: qualityScore.score,
|
||||
qualityReasons: qualityScore.reasons
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`✅ Risk check PASSED: All checks passed`, {
|
||||
todayPnL: todayPnL.toFixed(2),
|
||||
tradesLastHour: tradesInLastHour,
|
||||
@@ -159,3 +211,106 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface SignalQualityResult {
|
||||
passed: boolean
|
||||
score: number
|
||||
reasons: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Score signal quality based on context metrics from TradingView
|
||||
* Returns score 0-100 and array of reasons
|
||||
*/
|
||||
function scoreSignalQuality(params: {
|
||||
atr: number
|
||||
adx: number
|
||||
rsi: number
|
||||
volumeRatio: number
|
||||
pricePosition: number
|
||||
direction: 'long' | 'short'
|
||||
}): SignalQualityResult {
|
||||
let score = 50 // Base score
|
||||
const reasons: string[] = []
|
||||
|
||||
// ATR check (volatility gate: 0.6% - 2.5%)
|
||||
if (params.atr > 0) {
|
||||
if (params.atr < 0.6) {
|
||||
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 {
|
||||
score += 10
|
||||
reasons.push(`ATR healthy (${params.atr.toFixed(2)}%)`)
|
||||
}
|
||||
}
|
||||
|
||||
// ADX check (trend strength: want >18)
|
||||
if (params.adx > 0) {
|
||||
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)})`)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.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)
|
||||
if (params.pricePosition > 0) {
|
||||
if (params.direction === 'long' && params.pricePosition > 90) {
|
||||
score -= 15
|
||||
reasons.push(`Price near top of range (${params.pricePosition.toFixed(0)}%) - risky long`)
|
||||
} else if (params.direction === 'short' && params.pricePosition < 10) {
|
||||
score -= 15
|
||||
reasons.push(`Price near bottom of range (${params.pricePosition.toFixed(0)}%) - risky short`)
|
||||
} else {
|
||||
score += 5
|
||||
reasons.push(`Price position OK (${params.pricePosition.toFixed(0)}%)`)
|
||||
}
|
||||
}
|
||||
|
||||
const minScore = 60 // Require 60+ to pass
|
||||
return {
|
||||
passed: score >= minScore,
|
||||
score,
|
||||
reasons
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,12 @@ export interface ExecuteTradeRequest {
|
||||
timeframe: string // e.g., '5'
|
||||
signalStrength?: 'strong' | 'moderate' | 'weak'
|
||||
signalPrice?: number
|
||||
// Context metrics from TradingView
|
||||
atr?: number
|
||||
adx?: number
|
||||
rsi?: number
|
||||
volumeRatio?: number
|
||||
pricePosition?: number
|
||||
}
|
||||
|
||||
export interface ExecuteTradeResponse {
|
||||
@@ -360,6 +366,12 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
// Market context
|
||||
expectedEntryPrice,
|
||||
fundingRateAtEntry,
|
||||
// Context metrics from TradingView
|
||||
atrAtEntry: body.atr,
|
||||
adxAtEntry: body.adx,
|
||||
rsiAtEntry: body.rsi,
|
||||
volumeAtEntry: body.volumeRatio,
|
||||
pricePositionAtEntry: body.pricePosition,
|
||||
})
|
||||
|
||||
console.log('💾 Trade saved to database')
|
||||
|
||||
Reference in New Issue
Block a user