From 830468d52438108ae447ae48321631fa5dde96fe Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Thu, 30 Oct 2025 19:31:32 +0100 Subject: [PATCH] Implement signal quality scoring system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- app/api/trading/check-risk/route.ts | 155 +++++++ app/api/trading/execute/route.ts | 12 + lib/database/trades.ts | 2 + .../migration.sql | 3 + prisma/schema.prisma | 2 + workflows/trading/n8n-complete-workflow.json | 398 ------------------ workflows/trading/parse_signal_enhanced.json | 24 ++ 7 files changed, 198 insertions(+), 398 deletions(-) create mode 100644 prisma/migrations/20251030183114_add_rsi_and_price_position_metrics/migration.sql delete mode 100644 workflows/trading/n8n-complete-workflow.json create mode 100644 workflows/trading/parse_signal_enhanced.json diff --git a/app/api/trading/check-risk/route.ts b/app/api/trading/check-risk/route.ts index aaadaab..42ec907 100644 --- a/app/api/trading/check-risk/route.ts +++ b/app/api/trading/check-risk/route.ts @@ -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> { @@ -135,6 +143,50 @@ export async function POST(request: NextRequest): Promise 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 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 + } +} diff --git a/app/api/trading/execute/route.ts b/app/api/trading/execute/route.ts index 9426852..4966f83 100644 --- a/app/api/trading/execute/route.ts +++ b/app/api/trading/execute/route.ts @@ -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